🦋
Butterfly 用户手册
  • Introduction
  • 一 前言
  • 二 开始
    • 安装部署
    • 五分钟体验指南
    • 单机使用手册
    • 应用规范
      • handler specs
      • middleware specs
      • xingqiao_plugin specs
      • yiqiu_program specs
  • 三 客户端功能
    • MySQL 原生协议
    • MySQL ORM
    • Redis 原生协议
      • redis_config
      • redis_tls
    • Redis ORM
    • Redis mcpack
    • Localcache
    • Kazoo
  • 四 应用(通用服务)
    • API JSON 规范
    • 异步任务 BaiChuan(百川)
    • 任务调度 RuQi(如期)
    • 任务编排 XingQiao(星桥)
    • 配置管理 WuXing(五行)
    • 运筹决策 BaiCe(百策)
  • 五 部署运维
    • 单机容器化部署
    • 监控
    • 异常排查
      • CPU Load spike every 7 hours
    • 升级
    • 安全
    • 其他
  • 六 前端
    • butterfly_template
    • butterfly_fe
    • butterfly-admin(json2web)
      • amis
      • sso
      • pangu
    • NoahV
    • PyWebIO
  • 七 潘多拉魔盒
    • 装饰器
      • localcache_decorator
      • retry_decorator
      • custom_decorator
      • command2http_decorator
    • 算法
      • 算法-分位数
      • 算法-变异系数
    • 实用工具
      • host_util
      • shell_util
      • http_util
      • time_util
      • random_util
      • concurrent
      • jsonschema
      • blinker
      • toml
      • command_util
      • config_util
      • picobox
      • 对称加密
        • des
        • aes
      • ascii_art
        • ttable
        • chart
      • business_rules
      • python-mysql-replication
      • dict_util
    • 中间件
      • middleware_status
      • middleware_whitelist
    • test_handler.py
  • 八 最佳实践
    • 分布式架构
    • Code practice
    • Log practice
    • Daemon process
  • 附录
Powered by GitBook
On this page
  • 1 服务管理
  • 1.1 配置管理
  • 1.2 启动管理
  • 1.3 日志管理
  • 2 接口开发
  • 2.1 编写函数(根据返回值,区分为两种方式)
  • 2.2 路由注册
  • 2.3 访问接口函数
  • 2.4 handler 访问本目录文件
  • 3 手动测试 handler
  1. 二 开始

单机使用手册

1 服务管理

1.1 配置管理

1.1.1 配置全览

conf/config.py(APP 配置)

# coding:utf8
"""
Butterfly config
"""
import os

# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

SERVER_LISTEN_ADDR = ("0.0.0.0", 8585)
SERVER_THREAD_NUM = 16
SERVER_NAME = "Butterfly_app"
# Reqid prefix, two characters
SERVER_ID = "BF"
SERVER_IDC = "default"

# static
STATIC_PATH = "static"
STATIC_PREFIX = "static"

# DB
"""
# wuxing/ruqi use default database
"""
DATABASES = {
    "default": "mysql+retrypool://root:password@127.0.0.1:3306/test?max_connections=300&stale_timeout=300",
}

# Redis
"""
# eg1:"redis://[[username]:[password]]@localhost:6379/0"
# eg2:"redis://@localhost:6379/0?socket_timeout=2&socket_connect_timeout=1&retry_on_timeout=true"
"""
CACHES = {
    "default": "redis://@localhost:6379/0",
    # for cache: connect_timeout 100ms, read_timeout 200ms
    # "wuxing": "redis://@localhost:6379/0?socket_timeout=0.2&socket_connect_timeout=0.1&retry_on_timeout=false",
    # for mq: connect_timeout 500ms, read_timeout 2000ms
    # "baichuan": "redis://@localhost:6379/0?socket_timeout=2&socket_connect_timeout=0.5&retry_on_timeout=false",
}

# Local data or cache
LOCALDATA_DIR = os.path.join(BASE_DIR, "data")

# Auth
SECRET_KEY = None               # If it is None, a key will be randomly generated each time butterfly is started
JWT_TOKEN_TTL = 28800           # default 8 hours
HEADER_USERNAME = "HTTP_X_USERNAME"

# Scheduler
scheduler_store = "memory"      # ("mysql"/"memory")

# Adaptor
"""
# {"stat":"ERR", "data":{}} --> {"stat":"ERR", "success":False, "message":"ERR", "data":{}}
STAT_ADAPTOR={"status_name":"success", "status_map":{"OK":True,"default":False}, "message_name":"message"}
"""
STAT_ADAPTOR = None

config/logger_conf.py(日志配置)

# coding:utf8
"""
初始化配置日志

acclog : Butterfly 访问日志
errlog : Butterfly 错误日志
initlog: Butterfly 启动相关信息
"""
import os
import logging

from xlib import logger
from conf import config

# --------------------------LOG PATH-----------------------------
LOG_SIZE_LIMIT = 1024 * 1024 * 2
LOG_BATCH_WRITE = 0
PATH_INIT_LOG = os.path.join(config.BASE_DIR, "logs/init.log")
PATH_ACC_LOG = os.path.join(config.BASE_DIR, "logs/acc.log")
PATH_INFO_LOG = os.path.join(config.BASE_DIR, "logs/info.log")
PATH_WARNING_LOG = os.path.join(config.BASE_DIR, "logs/warning.log")
PATH_ERR_LOG = os.path.join(config.BASE_DIR, "logs/err.log")
PATH_CRIT_LOG = os.path.join(config.BASE_DIR, "logs/crit.log")

PATH_COMMON_LOG = os.path.join(config.BASE_DIR, "logs/common.log")
PATH_SCHEDULER_LOG = os.path.join(config.BASE_DIR, "logs/scheduler/scheduler.log")
PATH_SCHEDULER_WORKER_LOG = os.path.join(config.BASE_DIR, "logs/scheduler/scheduler-worker.log")

PATH_SERVICE_LOG = os.path.join(config.BASE_DIR, "logs/service.log")
PATH_RAL_LOG = os.path.join(config.BASE_DIR, "logs/ral/ral-worker.log")

# --------------------------LOG logger---------------------------
# 通用 logging 初始化( root logger ), 此日志没有 reqid
logger.init_log(PATH_COMMON_LOG)
# Butterfly scheduler 日志初始化, 此日志没有 reqid
logger.init_log(PATH_SCHEDULER_LOG, logger_name="apscheduler", level=logging.INFO)
logger.init_log(PATH_SCHEDULER_WORKER_LOG, logger_name="apscheduler_worker", level=logging.INFO)

# Butterfly service 日志初始化, 此日志含有 reqid
logger.init_bf_log(PATH_SERVICE_LOG, logger_name="service")
# Butterfly ral 日志初始化(Resource Access Layer), 此日志含有 reqid
logger.init_bf_log(PATH_RAL_LOG, logger_name="ral", level=logging.DEBUG)

critlog = logger.LoggerBase(PATH_CRIT_LOG, False, LOG_SIZE_LIMIT, LOG_BATCH_WRITE)
errlog = logger.LoggerBase(PATH_ERR_LOG, False, LOG_SIZE_LIMIT, LOG_BATCH_WRITE)
warninglog = logger.LoggerBase(PATH_WARNING_LOG, False, LOG_SIZE_LIMIT, LOG_BATCH_WRITE)
infolog = logger.LoggerBase(PATH_INFO_LOG, False, LOG_SIZE_LIMIT, LOG_BATCH_WRITE)
acclog = logger.LoggerBase(PATH_ACC_LOG, False, LOG_SIZE_LIMIT, LOG_BATCH_WRITE)
initlog = logger.LoggerBase(PATH_INIT_LOG, False, LOG_SIZE_LIMIT, LOG_BATCH_WRITE)

1.1.2 SERVER

SERVER_LISTEN_ADDR = ("0.0.0.0", 8585)  # 监听端口
SERVER_THREAD_NUM = 16                  # 线程数
SERVER_NAME = "Butterfly_app"           # 服务名标识,用于百川 worker

1.1.3 LOG

LOG_SIZE_LIMIT = 1024 * 1024 * 2                                    # 日志大小,大于此值时,日志文件将会被清空
LOG_BATCH_WRITE = 0
PATH_INIT_LOG = os.path.join(BASE_DIR, "logs/init.log")             # 启动日志,记录 handler 信息等
PATH_ACC_LOG = os.path.join(BASE_DIR, "logs/acc.log")               # 访问日志
PATH_INFO_LOG = os.path.join(BASE_DIR, "logs/info.log")
PATH_WARNING_LOG = os.path.join(BASE_DIR, "logs/warning.log")
PATH_ERR_LOG = os.path.join(BASE_DIR, "logs/err.log")               # 错误日志
PATH_CRIT_LOG = os.path.join(BASE_DIR, "logs/crit.log")
PATH_COMMON_LOG = os.path.join(BASE_DIR, "logs/common.log")         # logging 日志
PATH_COMMON_BF_LOG = os.path.join(BASE_DIR, "logs/common_bf.log")   # logging 日志

1.1.4 STATIC

STATIC_PATH = "static"                  # 静态文件路径,默认为 {butterfly_project}/static/{file_path}
STATIC_PREFIX = "static"                # 静态文件前缀,判断请求 URL 是否为请求静态文件

1.1.5 DB

"""
# wuxing/ruqi use default database
"""
DATABASES = {
    "default": "mysql+retrypool://root:password@127.0.0.1:3306/test?max_connections=300&stale_timeout=300",
}

1.1.6 Redis

"""
# eg1:"redis://[[username]:[password]]@localhost:6379/0"
# eg2:"redis://@localhost:6379/0?socket_timeout=2&socket_connect_timeout=1&retry_on_timeout=true"
"""
CACHES = {
    "default": "redis://@localhost:6379/0",
    # for cache: connect_timeout 100ms, read_timeout 200ms
    # "wuxing": "redis://@localhost:6379/0?socket_timeout=0.2&socket_connect_timeout=0.1&retry_on_timeout=false",
    # for mq: connect_timeout 500ms, read_timeout 2000ms
    # "baichuan": "redis://@localhost:6379/0?socket_timeout=2&socket_connect_timeout=0.5&retry_on_timeout=false",
}

1.1.7 LOCALDATA

LOCALDATA_DIR = os.path.join(BASE_DIR, "data")

存储本地数据目录

1.1.8 AUTH

SECRET_KEY = None                   # If it is None, a key will be randomly generated each time butterfly is started
JWT_TOKEN_TTL = 28800               # default 8 hours
HEADER_USERNAME = "HTTP_X_USERNAME"
  • SECRET_KEY : jwt SECRET_KEY

  • JWT_TOKEN_TTL : jwt token ttl

  • HEADER_USERNAME : 用户名 handler

    • 请求方法:若此项配置的 "HTTP_X_USERNAME" 则请求时可传 -H "x_username:meetbill" header

1.1.9 Scheduler

scheduler_store = "memory"      # ("mysql"/"memory")

主要用于【如期】使用

1.1.10 Adaptor

"""
# {"stat":"ERR", "data":{}} --> {"stat":"ERR", "success":False, "message":"ERR", "data":{}}
STAT_ADAPTOR={"status_name":"success", "status_map":{"OK":True,"default":False}, "message_name":"message"}
"""
STAT_ADAPTOR = None

用于扩展 API 接口的状态字段

STAT_ADAPTOR 需要有三个信息

  • status_name: 状态名

  • status_map: 状态映射,默认值为 "default" value, 若可以匹配到映射中的值,则以映射值为值

  • message_name: 信息名

1.2 启动管理

Usage: run.sh {start|stop|restart|status}
 start          Start butterfly processes.
 docker_start   Start butterfly processes for docker.
 stop           Kill all butterfly processes.
 restart        Kill all butterfly processes and start again.
 status         Show butterfly processes status.

备注:bash run.sh docker_start 用于打 docker 镜像时使用

如何修改启动项标识

在日常使用中可能会在同一台机器上启动多个 butterfly,不同的 butterfly 提供着不同的服务,可能是测试服务,可能是做配置管理服务使用的,等等,当然这些 butterfly 的端口是不一样的,那如何根据进程看出是什么服务尼?

只需要在服务启动前修改下,run.sh 的 PROC_SIG="954216e9" 修改为想要使用的用途即可,比如 PROC_SIG="test_dev"

注:此项修改需要在服务启动前修改,因为 run.sh 对进程的管理,也会对 PROC_SIG 进行匹配

1.3 日志管理

1.3.1 启动日志

启动相关信息日志会记录在 __stdout

如果启动失败,可以在这个日志文件中找到原因

1.3.2 butterfly 框架日志

1.3.2.1 acc 访问日志

acc 日志路径在 logs/acc.log

日志内容

2021-02-08 21:35:30    91640    /meetbill/butterfly/xlib/httpgateway.py:259    127.0.0.1    1FA15BDE4B6CD7A0    GET    /demo_api/hello    cost:0.000162    stat:OK    user:-    talk:ceshi1=0.098;ceshi2=0.002    params:str_info=hello    error_msg:    res:ceshi2=5.4;ceshi1=5.3

备注:
(1) access 日志中各 field 使用 '\t' 进行分割
(2) COST,STAT,USER,TALK,PARAMS,ERROR_MSG,RES field 会有 field 前缀,使用 `:` 进行分割
    field 中多个 item 使用 `;` 进行分割,每个 item 使用 `=` 进行分割 item_name 和 item_value
    即:{field_prefix}:{item_name1}={item_value1};{item_name2}={item_name2}
    如:talk:ceshi1=0.098;ceshi2=0.002
  • DATE: 2021-02-08

  • TIME: 21:35:30

  • PID : 91640

  • CODE_INFO: /meetbill/butterfly/xlib/httpgateway.py:259

  • IP: 127.0.0.1

  • REQID: 1FA15BDE4B6CD7A0 (请求 ID)

  • METHOD: GET

  • PATH: /demo_api/hello

  • COST: cost:0.000162 (整体耗时,单位是秒)

  • STAT: stat:OK (执行结果状态,若自定义 HTTP 方式时,则此处会是 HTTP 状态码)

  • USER: user:- (用户名)

  • TALK: talk:ceshi1=0.098;ceshi2=0.002 (代码片段耗时,比如一个 SQL 语句,请求 Redis 的耗时等)

  • PARAMS: params:str_info=hello (请求参数)

  • ERROR_MSG: error_msg:(错误信息)

  • RES: res:ceshi2=5.4;ceshi1=5.3 (需要记录的结果数据信息)

在 handler 逻辑中可以通过如下方式写入日志到 logs/acc.log

记录额外参数

req.log_params["x"] = 1

记录程序运行耗时

# req.start_timming() 为可选项,若不加此语句,则默认以本次请求开始记录耗时
req.start_timming()
...
req.timming("ceshi1") # 会记录 req.start_timming() 到此行的耗时
...
req.timming("ceshi2") # 会记录 req.timming("ceshi1") 到 req.timming("ceshi2") 间的耗时

将会打印日志如下:
ceshi1=0.001;ceshi2=0.003

记录错误信息(推荐)

req.error_str = err_msg

使用此方式,错误日志不但可以记录到 acc.log , 而且响应头中也会返回给客户端此信息,如下:

reply: 'HTTP/1.1 200 OK\r\n'
header: ruqi: 1.0.1
header: Content-Length: 15
header: butterfly: 1.1.4
header: x-reqid: 3B2BBCF470912A79
header: x-cost: 7.732843
header: x-reason: u'Job identifier (test1) conflicts with an existing job'
header: Connection: close
header: Date: Sat, 02 Jan 2021 01:18:47 GMT
header: Server: XXXXXXXXXXXXXXXX
{u'stat': u'ERR'}

记录 response 信息

req.log_res:(set)

代码:
req.log_res.add("test1=info")

日志:
... res:test1=info

1.3.2.2 err 异常日志

err 日志路径在 logs/err.log

例子(不常用,主要用于 butterfly 框架捕获未处理的异常逻辑使用)

req.log(self._errlog, "Json dump failed\n%s" % traceback.format_exc())

req.log 用于记录异常日志以及其他类型日志,日志中会记录 reqid, 需要传入 logger 和日志信息

1.3.2.3 init 日志

init 日志路径在 logs/init.log, 日志中会记录如下信息

  • 日志中会打印自动加载的 handler 的日志信息

  • 如果开启了 scheduler, 则打印 scheduler 是否启动成功

1.3.3 自定义日志输出

acc.log 和 err.log 均为框架本身产生的日志,用于接口统计以及服务异常排查问题等等

这里的自定义日志内容有两种方式

  • 将需要的日志,通过接口写入到 acc.log

  • 使用 logging 模块『推荐』,将日志写入 common.log 和 common.log.wf

这个来说明下如何使用 logging 将日志写入 common.log 和 common.log.wf

from xlib import retstat
from xlib.middleware import funcattr
import logging

logger = logging.getLogger("service")

__info = "test_api"
__version = "1.0.1"

@funcattr.api
def hello(req, str_info):
    isinstance(req, Request)
    # 打印 info log
    logger.info("test info log")
    # 打印 error log
    logger.error("test error log")
    return retstat.OK, {"data": {"str_info": str_info}}, [(__info, __version)]

备注:

如果使用了 logging 日志记录方式
(1) 默认 info 级别以上的 logging 日志都会打印到 config.py 设置的 PATH_COMMON_LOG = "logs/common.log" 日志中
(2) 默认 error 级别以上的 logging 日志都会打印到 logs/common.log.wf 中
(3) 如果是用户发起的请求 (handler 应用程序),logging 日志中会默认加上 reqid, 如果没有获取到 reqid(比如 Main 线程) 则会打印 "XXXXXXXXXXXXXXXX"

2 接口开发

2.1 编写函数(根据返回值,区分为两种方式)

2.1.1 推荐方式

(1) 函数文件

handlers/demo_api/__init__.py

例子

@funcattr.api
def hello(req, str_info):
    """
    带参数请求例子

    Args:
        req     : Request
        str_info: (str)
    Returns:
        json_status, Content, headers
    """
    isinstance(req, Request)
    return retstat.OK, {"str_info": str_info}, [(__info, __version)]

返回值分别为:状态信息字段,数据信息字段, 自定义 HTTP 报文头信息列表

2.1.2 自定义 HTTP 码返回方式

(1) 函数文件

handlers/demo_httpapi/__init__.py

(2) 编写对应方法

  • 参数

    • 第一个参数为 req, 固定

  • 返回值 (httpstatus, [content], [headers])

    • httpstatus: (int) 必须有

    • content: (str/dict) 非必须(当返回值为 2 个的时候,第 2 个返回值为 Content)

      • 当 content 为 dict 时,会自动转为 json ,并且设置 header("Content-Type","application/json")

      • 当 content 为其他时,会自动设置为 ("Content-Type","text/html")

    • headers: 非必须(当返回值为 3 个的时候,第 3 个返回值为 headers)

      如:

      def hello(req, str_info):
      isinstance(req, Request)
      return retstat.HTTP_OK, {"stat":"OK","str_info": str_info}, [(__info__, __version__)]

      返回值分别为:HTTP 返回码,状态信息字段 + 数据信息字段, 自定义 HTTP 报文头信息列表

2.2 路由注册

(1) 函数文件

wsgiapp.py

handlers package 和 handlers 下的 package 都会默认注册到路由中

2.3 访问接口函数

get 请求

Request:

curl -v "http://ip:port/demo_api/hello?str_info=happy"

如:
curl -v "http://127.0.0.1:8585/demo_api/hello?str_info=happy"

同时可以使用 -v 查看额外 headers 信息,如下:

Response:

* Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 8585 (#0)
> GET /demo_api/hello?str_info=happy HTTP/1.1
> Host: 127.0.0.1:8585
> User-Agent: curl/7.54.0
> Accept: **/**
>
< HTTP/1.1 200 OK
< meetbill: 1.0.1  --------------------------------- 函数中返回的 headers 信息
< Content-Length: 35 ------------------------------- Content 的长度
< butterfly: 1.1.3 --------------------------------- Butterfly 版本信息
< x-reqid: C31582ED1FBBFA2D ------------------------ 请求 id,此 id 会打印到日志文件中
< x-cost: 0.003114 --------------------------------- 耗时 (time.time() UNIX 时间戳
<                                                    格林尼治时间 1970 年 01 月 01 日 00 时 00 分 00 秒 起至现在的总秒数之差)
< Date: Mon, 04 Mar 2019 09:33:26 GMT
< Server: XXXXXXXXXXXXXXXX
<
* Connection #0 to host 127.0.0.1 left intact
{"stat": "OK", "str_info": "happy"}%

post 请求

request:

curl -v   -d '{"str_info":"happy"}' http://127.0.0.1:8585/demo_api/hello

Response:

* Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 8585 (#0)
> POST /demo_api/hello HTTP/1.1
> Host: 127.0.0.1:8585
> User-Agent: curl/7.54.0
> Accept: */*
> Content-Length: 20
> Content-Type: application/x-www-form-urlencoded
>
* upload completely sent off: 20 out of 20 bytes
< HTTP/1.1 200 OK
< meetbill: 1.0.1
< Content-Length: 35
< butterfly: 1.1.3
< x-reqid: 1A805D21CC791F3E
< x-cost: 0.000142
< Date: Mon, 04 Mar 2019 11:48:04 GMT
< Server: XXXXXXXXXXXXXXXX
<
* Connection #0 to host 127.0.0.1 left intact
{"stat": "OK", "str_info": "happy"}

备注 1 :JSON 格式数据属性必须用双引号包裹

var json = '{"name":"imooc"}'; // 这个是正确的 JSON 格式
var json = "{\"name\":\"imooc\"}"; // 这个也是正确的 JSON 格式
var json = '{name:"imooc"}'; // 这个是错误的 JSON 格式,因为属性名没有用双引号包裹
var json = "{'name':'imooc'}";// 这个也是错误的 JSON 格式,属性名用双引号包裹,而它用了单引号

备注 2 :可传多级 json

# Request:
curl -d '{"str_info":{"field1": "value1", "field2": "value2"}}' http://127.0.0.1:8585/demo_api/hello

# Response:
{"stat": "OK", "str_info": {"field2": "value2", "field1": "value1"}}

python post 方式提交 json 字符串

import urllib2
import json

data = {"str_info":"hello info"}

headers = {'Content-Type':'application/json'}
request = urllib2.Request(url='http://127.0.0.1:8585/demo_api/hello',headers=headers,data=json.dumps(data))
response = urllib2.urlopen(request)

print response.read()

2.4 handler 访问本目录文件

通过 cur_path 与文件进行拼接

cur_path = os.path.split(os.path.realpath(__file__))[0]

3 手动测试 handler

测试方法

$ python test_handler.py
Previous五分钟体验指南Next应用规范

Last updated 9 months ago