支付系统0x01: 基础设施 & 初版架构

date: 2017-11-21 22:10:38
title: 支付系统0x01: 基础设施 & 初版架构

一周紧张的开发后, 支付系统的大致雏形已经出来的, 需要完善的地方还有很多(每天都是长长的 todolist), 这里先讲一讲基础设施和初版架构.

基础设施篇

docker

既然是微服务设计, 当然跑不了 docker 的使用. 这里不赘述, 单从工具之美的角度聊一聊.

核心概念:

  • repository 仓库: 镜像的仓库
  • image 镜像: 定义程序运行的环境
  • container 容器: 实例化的镜像, 程序的运行环境, 通常一个 container 代表一个 service(服务)

备注: container == service, 之后不再重申这个概念

没错, 核心概念就这么多, 下面再来看看 周边, 我喜欢简单归纳到工具这个慨念里面:

  • docker hub, 类似 github, 你也可以类比 repository/image 的概念和你的 git 项目来理解, 我一般使用国内镜像源(aliyun)替代
  • dockerfile: 一系列指令来构建一个 image
  • dockerd: docker server 端程序
  • docker: docker client 端程序, 用来和 docker server 通信
  • docker-compose: container 编排程序, 适合服务较少场景(比如开发环境)下使用

好了, 差不多知道这么多概念就可以动手了:

备注: 使用 docker-for-window 一站式解决 docker 安装, 之后有 docker 提供 linux 环境, 完全可以把 window 变成很好的开发平台.

好了, 说了这么多, 直接实战, 你就会发现这个到底有多简单了, 先来看 docker:

docker help # 这个不用多说吧
docker info # 查看 docker 信息, 一般用来确认 docker 安装启动是否正常

docker pull image:tag # 熟悉 git 对 tag 就不陌生了吧
docker images # 参看本地镜像
docker rmi $(docker images -f "dangling=true" -q) # 高级点的应用, 删除所有镜像

docker build -t image:tag xxxDockerfile # 根据 dockerfile 构建 image

docker ps # 查看运行中的容易, -a 查看所有
docker rm `docker ps -a -q` # 删除所有容器, -q 只显示 container id

# run 运行容器, 可以说是最复杂的命令了, 这里直说常用的
docker run --name xxx-app -d -p 8080:80 image:tag # 运行容器 + 命名(--name) + daemon(-d) 运行 + 绑定端口(-p)
docker run -ti --rm centos:7.4 bash # 我常用这个来开一个 linux 环境, -ti 如同终端一样操作, --rm 退出 container 后自动删除

看到这里, 你可能还没有感受到 docker 的强大, 但是请你注意这里:

docker run -ti --rm centos:7.4 bash, 你就执行一下这条命令, 我就有了一个 centos 7.4 的环境随便你折腾了, 对的, 就一条命令!

备注: 实际使用中 docker 命令其实也不怎么用, 主要还是使用 doc(alias doc='docker-compose' 后面不再提示)

然后再来看 dockerfile, 首先为什么会有 dockerfile 呢? 我的理解是:

将软件的运行环境 文本化/指令化, 变成程序源码这种容易分发的方式.

dockerfile 作为一系列指令(用来构建运行环境)的集合, 实在是简单得无须多讲, 这里就简单列举一下这次支付系统使用到的例子:

# mysql.Dockerfile
FROM mysql:8 # 官方镜像
MAINTAINER daydaygo <1252409767@qq.com> # 镜像维护者
ADD my.cnf /etc/mysql/conf.d/my.cnf # 添加配置文件
RUN chmod -R 644 /etc/mysql/conf.d/*
CMD ["mysqld"] # 运行 mysqld 服务
EXPOSE 3306 # 开启 3306 端口

# redis.Dockerfile
FROM redis:4.0-alpine
MAINTAINER daydaygo <1252409767@qq.com>
COPY redis.conf /etc/redis/redis.conf
CMD redis-server /etc/redis/redis.conf --appendonly yes

这样, 我系统里就有了随时可用的 mysql 和 redis 服务了.

这里说一下 dockerfile 的相关的学习方法:

  • 大致看一下 dockerfile 指令, 一些容易混淆的指令注意一下(这个教程里都会突出的)
  • 使用官方镜像, 务必大致浏览官方提供的 readme, 常见用法基本都可以在这里找到
  • 务必 看一下官方镜像的 dockerfile, 这样可以帮你有效解决 环境依赖(比如 php 镜像使用 pecl 安装插件需要安装哪些软件)/环境变量(有哪些变量, 怎么使用) 相关的问题

好了, 到 doc 登场了, 按照上面的 dockerfile, 我们建好了一个又一个服务, 怎么愉快的玩耍起来呢? 直接看 「栗子」:

  • 首先, 你要有一个 docker-compose.yml 的配置文件, 基于 yaml 语法(和 json 一样简单, 出现原因大概是 xml 实在是太长了)
version '3'
services: # 服务编排
    nginx:
        build: # 需要 dockerfile 来构建镜像
            context: ./server
            dockerfile: nginx.Dockerfile
        volumes:
            - ../:/var/www # 挂载项目目录
            - ./logs/nginx/:/var/log/nginx # 挂载 nginx 日志
        links:
            - fpm # 需要使用 fpm service
        ports: # 端口绑定
           - "80:80"
           - "443:443"
    fpm:
        build:
            context: ./server
            dockerfile: fpm.Dockerfile
        volumes:
            - ../:/var/www
            - ./logs/fpm/:/var/log/php7
        links:
            - mysql
            - redis
    mysql:
        build:
            context: ./server
            dockerfile: mysql.Dockerfile
        volumes:
            - ./data/mysql:/var/lib/mysql # 挂载数据目录, 实现数据持久化
        ports:
            - "3306:3306"
        environment: # 设置环境变量
            # MYSQL_DATABASE: test
            # MYSQL_USER: test
            # MYSQL_PASSWORD: test
            MYSQL_ROOT_PASSWORD: root
    redis:
        build:
            context: ./server
            dockerfile: redis.Dockerfile
        volumes:
            - ./data/redis:/data
            - ./logs/redis:/var/log/redis

有了 docker-compose.yml 后, 运行起来就好了:

doc up -d nginx # daemon 状态运行 nginx, 因为 nginx 需要 fpm, fpm 需要 mysql redis, 所有 4 个服务都会运行起来
doc up -d --build nginx # 重新构建 image 并运行

doc stop # stop service
doc logs xxxService # 查看 service 日志, 在 service 启动失败时会有报错信息, 如果报错信息不足以帮助解决问题, 就可以使用这个查看一下日志来获取更多信息

到这里你可能会说怎么弄个 lnmp 环境这么麻烦, 杂七杂八的折腾了这么久, 工具一个接着一个, 但是请关注一下结果, doc up -d nginx, 只要执行一下这个, 你的 lnmp 好了, 而且你想用什么版本就用什么版本, 更主要的是:

快, 这才是老司机的本质.

作为一个使用 docker 2年+ 的老司机, 继续安利一波, docker 当做工具来使用真的非常非常简单, 大致了解完基础后, 再去看看几个开源项目怎么使用的就可以用起来了, 比如下面这几个:

  • laradock: 这里顺便安利一下 laravel 框架, 不一定要用它, 但是从他丰富的 周边 真的可以学到很多
  • swoole-docker: swoole distribution 的 docker 运行环境, 作为 php 程序员, 去了解一下 swoole 吧, 你会发现很多很有意思的人, 而不是 「php是最好的语言」 或者 「php 真 low」

想深入的话推荐这本书: <容器与容器云ed2>

alpine linux

这里再补充一点 alpine linux 的知识. 各种官方镜像, 实际也是在 linux 系统上, 通过安装软件的方式构建而成, 通常我们把这个 linux 系统叫做 基础镜像. 现在比较流行的是 2 个 Debianalpine. Debian 我不多说了, 同系的 Ubuntu 以及 redhat 系的 centos 都很常见, 这里重点推荐一下 alpine linux.

  • 首先是镜像容量对比: 几M vs 接近百M
  • 然后是 alpine 的 工具 属性: 从 busybox 这个继承了上百个常用 linux 命令的工具集升级而来. 在我眼里 alpine = linux kernel + tool(包管理其实也是为了增减工具)

使用这么久下来, 通过笔记(wiki - os - linux 发行版)来看, 实在是不要太简单:

# apk 包管理
echo -e "http://mirrors.aliyun.com/alpine/v3.4/main\nhttp://mirrors.aliyun.com/alpine/v3.4/community" > /etc/apk/repositories
apk add xxx
apk search -v xxx
apk info -a xxx
apk info

# network
apk add iproute2 # ss vs netstat
ss -ptl

git

关于 git, 能说的太多了, 比如 <pro git> 这本公版书. 这里简单提四个点.

  1. git 是 Linus 开发的. Linus 还开发了 linux (就是这么牛逼). 而 git 之所以叫 git, 是因为我就是一个自私的混蛋, 做的东西都喜欢用自己来命名, linux 是, git 也是. 另一个经典语录是一次公开场合下 I am your god.

  2. 当然绕不开 git 和 svn 的对比. 本质是 分布式系统 vs 集中式系统, 更简单一点是, 你没有一个远程仓库, 你依旧可以可以开心的和 git 玩耍, 远程仓库的功能是帮助你分发和协作. 总结一下: 珍爱生命, 原理 svn. (发邮件出文件更新列表来上线的日子, 我在三年前刚工作的时候也经历过, 没想到现在还能碰到, 活久见)

  3. git flow 工作流, 这种方式比较复杂, 但是非常推荐你去了解一下, 当你见识到了 终极复杂 的方式, 按需精简成需要的工作流就简单了. 这里给几个我使用下来的经验: master(主) 分支是 产品 分支, 要保证一直可用, 所以 merge(合并) 到 master 分支需要具有一定能力(权限); 至少保留 dev 分支作为测试使用; 开发时间较长的需求务必使用 feature(特性/功能) 分支; 上线使用 tag(标签)

  4. 当然还是 git 常用功能小结了:

git status # 查看发生改动的文件列表
gitk # 图形化工具查看文件改动; 安装 git 后自带的图形化界面, 可以直接命令行调起, 我本人强烈推荐, 我不安装其他 git 图形化工具
# 使用 gitk 确认文件修改无误 -> 一定要自己 review 一下代码, 你自己都不愿意 code review, 就不要憧憬那些有 code review 的牛逼团队了
# 在 review 之前, 一定要自测一遍自己的代码, 不要没事就给测试妹子刷存在感
git checkout . # 取消所有修改
git checkout xxxFile # 取消某个文件的修改
git add -A # 添加所有修改
git commit -m 'commit message' # 提交修改

git push # 提交代码到远程分支, 这里注意, 远程仓库一般使用 origin 作为名称, 这里省略了
git pull # 从远程仓库获取最新代码

git fetch # 从远程获取 branch/tag 信息
git branch # 查看本地分支, -r 查看远程分支, -a 查看所有
git checkout dev # 切换到 dev 分支, -b 新建并切换到分支
git merge dev # 将 dev 分支 merge 到当前分支

git tag # 查看 tag
git tag -a v1.0 -m 'tag message' # 添加 tag
git push --tags # 同时推送 tag 到远程仓库

git stash # 暂存本地修改
# do something else
git stash pop # 恢复暂存的修改

git remote set-url origin git://new.url.here # 修改远程仓库地址, 我经常会用到这个
git help xxxCmd # 查看帮助文件, 这里会用浏览器打开 html 文件, 非常方便
git config --global user.name "daydaygo" # git 设置, --global 全局生效

# pull request & code review
# 1. fork(复制) 原项目到自己的仓库
git clone origin-git-url # 克隆自己的仓库
// changge - commit
# 2. 在 gitub/gitlab 提交 pr(pull request), 原项目上就可以看到, 之后提交后会自动提 pr
git remote add upstream upstream-git-url # 添加原仓库地址; 取名为 upstream, 和 origin 一样,习惯而已
git pull upstream master # 从原仓库获取更新

还有一个重要概念是 冲突, 其实很简单, 你的版本和其他人的版本无法合并, 比如你写了 a=1, 别人写了 a=2, 所以, 和当事人确认一下代码, 改一下再提交就好.

周边 还要很多, 比如 .gitignore / .gitkeep 文件, 慢慢 get 就好.

<只是为了好玩 - Linus自传>, 强烈推荐这本书, 也许可以境界提升.

gitlab

讲完 git, 就会发现, 少一个 远程仓库, 对, gitlab 就是干这的. 当然, 他的功能远不止于此:

  • web 操作界面
  • 仓库的权限管理: 用户 / 用户组 / https & ssh & deploy key
  • pull request & code review
  • CI 持续集成

因为使用时间并不长(还有一个原因是规模并不大), 探索目前只到这个阶段, 这里晒一下 CI 的成果:

image

过程有点小坎坷, 不过还是套用之前 blog 里提到的观点: 唯手生尔, 这里简单叙述一下:

  • gitlab ci 的工作原理: 安装 gitlab-runner 服务并执行 gitlab-runner register 和 gitlab 上的项目关联
  • 安装 gitlab-runner 按照官网的文档来就好了, 测试了 shell 版和 docker 版, 最终还是选用了 docker 版, 并且使用了 阿里云镜像仓库, 保存带 phpunit 的 php 镜像
  • master分支/tag 变更触发 CI, gitlab 会按照 .gitlab-ci.yml 中定义的任务创建一个 job, job 分发给关联的 gitlab-runner, runner 会先跟新项目, 然后运行 job.

使用 docker 版的 gitlab-runner 也非常简单, 还是使用 doc 进行编排:

services:
    ci:
        image: gitlab/gitlab-runner:alpine
        volumes:
            - ./data/gitlab-runner/config:/etc/gitlab-runner
            - /var/run/docker.sock:/var/run/docker.sock:Z

然后执行:

doc up -d ci # 启动 gitlab-runner 服务
doc exec ci gitlab-runner register # 执行注册管理, 可以多次执行, 定义多个 runner

.gitlab-ci.yml 定义非常简单:

before_script:
  - cd pay-support

phpunit: # 类似这样定义一个又一个任务就好了
  script:
    - phpunit --coverage-text --colors=never

基础设施到这里先告一段落, 还有 监控报警/日志收集分析 等, 下次再做分解.

初版架构篇

其实在上一篇 支付系统0X00: 支付系统预研 就已经将架构方面的内容侃得七七八八了, 多是在实施的过程中继续参考 凤凰牌老熊 - 现代支付系统设计 中各章节, 在细节上继续下功夫.

一周时间毕竟有限, 还需要细细打磨

初版概况

流程图 / 项目结构图: https://www.processon.com/view/link/5a0baa75e4b049e7f4fd90f4

  • 核心业务流程: 用户 -> 商户 -> 支付接口(验参验签) -> 支付路由 -> 支付方式 -> 收单 -> 支付成功
  • 架构分层: 产品服务 核心系统(支付核心 + 支付服务) 支撑系统

系统边界:

  • 支付网关: api路由 -> 聚合支付; 接口安全
  • 支付产品: 风控 支付路由 参数校验 支付流程(交易记录, 支付渠道, 同步/异步通知)
  • 支付渠道: 和支付渠道对接, 按照支付产品预定格式化统一化结果输出

项目结构:

  • pay-gateway: 支付网关, 使用 api 和商户交互
  • pay-product: 封装支付产品
  • pay-channel: 支付渠道 sdk, 和支付渠道交互
  • pay-lib: 依赖库
  • pay-support: 支撑系统, 目前只包含 api doc 和 phpunit

重要实现细节:

  • 商户模式: 通过支持商户模式, 让支付系统具有平台属性, 方便业务拓展
  • 请求参数设计: 公共参数 + 业务参数 + 业务拓展参数, 保持接口的灵活
  • 请求全异步化机制: 接到请求后立刻返回请求是否成功受理 + 异步处理耗时任务 + 异步通知商户

基于 swoole 的任务队列

感谢开源作品: swoole-jobs

从上面的实现细节 请求全异步化机制 可知, 支付系统引入了任务队列, 而且相当重要. 原来项目基于 yii2 console + redis queue + yii2 mutex 实现了一套任务机制, 最终基于 crontab 运行. 核心实现细节如下:

// mutex 使用
$lock = \yii::$app->mutex->acquire( $lock_name );
\register_shutdown_function(function() use($lock_name) {
    return \yii::$app->mutex->release( $lock_name );
});

// 基于 redis list 实现的任务队列
public static function push($params = []) {
    $redis = self::getRedis($params);
    return $redis->executeCommand('RPUSH', $params);
}
public static function pop($params = []) {
    $redis = self::getRedis($params);
    return $redis->executeCommand('LPOP', $params);
}

// 执行任务
$lock = CommonHelper::lock(); // 封装 mutex 的使用
if (!$lock) {
    return self::EXIT_CODE_ERROR;
}
$tag_file = '/tmp/xxx.tag'.$id; // yii2 console 应用 bug, 检测到 tag 后关闭脚本
$content = RedisQueue::pop([RedisQueue::LIST_CHANNEL_FEEDBACK]); # 执行任务

当然, 这样的结构完全可以胜任 任务队列 这一定义, 并且经历过线上检验, 比如说添加 tag 来关闭脚本 这样的经验积累. 不过考虑再三, 还是决定引入 swoole-jobs 来构建新的任务队列, 理由如下:

  • 原来任务系统耦合在原有项目中, 需要做一定程度拆分重构才能单独成为任务队列
  • 服务实现依赖 crontab, 就会受到 crontab 的局限, 比如 1 分钟运行一次, 需要多进程处理需要自己实现

而 swoole-jobs 在设计上的优势很明显:

  • 基于 swoole process 实现服务化和进程管理, 修改配置项就可以改变进程数量, 同时开几百个进程没问题 (感谢 rango 大大的及时响应, 当时 @ 的时候还挺虚的)
  • 抽象 Queue 实现, 可以基于 redis / rabbitmq 等不同驱动, 灵活拓展更多队列功能, 比如 topic 订阅, 任务优先级
  • Job 类抽离, 可以有更多 Job 运行的方式: 类似 yii2 的 console 应用, 类静态方法调用, 类动态方法调用, 闭包

而所有这些, 不到 10 个文件 500 行代码, 推荐大家阅读源码.

欢迎 star / fork / 提交 pr, 我提交的 pr 已被作者采纳了哦.

支撑系统: api doc + phpunit

我以前也是那种啪啪啪疯狂输出, 然后提交完代码就 万事大吉 类型的, 现在再想来, 好听点叫 too young, 其实就是 low, 还好遇到了各种大大, 而不至于过早夭折.

用电竞圈的一个段子: 躲在大哥胯下疯狂输出.

谈到 大哥, 就不得不感谢一下一路走来遇到的各位腾讯技术出身的大大 -- 两任 CTO, 吃着中药熬着夜的刀哥, 传授了N多经验的红旗; 开发出 swoole 的 rango, 让我坚定 php 可以走的更远; 2大php开源项目的核心开发者朱新宇, 勇敢的少年啊快去创造奇迹.

回到正题, 软件开发是一个很难杜绝错误但是尽量要求 正确 并且 持续正确 的工作.

所以, 下面几点你当做最佳实践也好, 技能提升也好, 或者技术团队管理也好, 但是请你记住, 知道有这些方式方法经验教训的存在:

  • 上面 git 部分的内容, 这里再重审一遍, 自己的写的代码, 一定要自测通过, 如果被发现没有自测并且存在明显(或者说低级)错误(你还好意思称之为 bug 么), 会受到集体鄙视;
  • 提交代码前, 务必自己 review 一下, 那些调试语句没删掉, 数据库修改忘添加等, 都可以通过这道工序解决掉
  • 接口开发一定要提供接口文档(api doc)

这里推荐一个 api 文档工具, swagger ui, 使用非常简单, 只用编写 yaml 文件(这个多次碰到了, 赶紧 get 这个技能吧)就行. swagger ui 是静态页面, 放到 web server 下运行即可.

支付系统 api 文档: http://hp-api.daydaygo.top (有 ip 限制, 公司内才可以访问)
也可以直接看官方的 demo: http://petstore.swagger.io/ (实际使用并不会用到这么多特性, 会非常简单)

  • 测试先行, 或者说测试驱动开发, 是有效发现并减少 bug 的方法, 而且 phpunit 真的很简单易用, 上面的 gitlab ci, 就是集成了 phpunit 测试

phpunit 快速上手:

  1. 全局可执行文件: composer global require phpunit/phpunit
  2. 项目中添加依赖: composer require phpunit/phpunit --dev
  3. 添加 phpunit.xml 配置文件: 用一个现有文件作为模板改改就好
  4. 编写测试用例: 一般添加 unit test 作为功能测试(类, 函数 等), feature test 作为 api 接口测试

写在最后

行文至此, 颇有种 酣畅淋漓 之感.

也许这就是对自己所做的事产生的自豪感吧, 谦虚点是自鸣得意, 吹起牛皮来就是独孤求败了.

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,905评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,140评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,791评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,483评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,476评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,516评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,905评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,560评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,778评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,557评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,635评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,338评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,925评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,898评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,142评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,818评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,347评论 2 342

推荐阅读更多精彩内容