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 安裝: 直接使用 阿里云 - 容器服務 - 鏡像加速器, 里面有詳細的文檔
- docker-compose 安裝: 直接使用 daocloud 軟件中心; 我通常會配置
alias doc='docker-compose'
這樣的別名.
備注: 使用 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 個 Debian
和 alpine
. 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> 這本公版書. 這里簡單提四個點.
git 是 Linus 開發的. Linus 還開發了 linux (就是這么牛逼). 而 git 之所以叫 git, 是因為我就是一個自私的混蛋, 做的東西都喜歡用自己來命名, linux 是, git 也是. 另一個經典語錄是一次公開場合下 I am your god.
當然繞不開 git 和 svn 的對比. 本質是 分布式系統 vs 集中式系統, 更簡單一點是, 你沒有一個遠程倉庫, 你依舊可以可以開心的和 git 玩耍, 遠程倉庫的功能是幫助你分發和協作. 總結一下: 珍愛生命, 原理 svn. (發郵件出文件更新列表來上線的日子, 我在三年前剛工作的時候也經歷過, 沒想到現在還能碰到, 活久見)
git flow 工作流, 這種方式比較復雜, 但是非常推薦你去了解一下, 當你見識到了 終極復雜 的方式, 按需精簡成需要的工作流就簡單了. 這里給幾個我使用下來的經驗: master(主) 分支是 產品 分支, 要保證一直可用, 所以 merge(合并) 到 master 分支需要具有一定能力(權限); 至少保留 dev 分支作為測試使用; 開發時間較長的需求務必使用 feature(特性/功能) 分支; 上線使用 tag(標簽)
當然還是 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 的成果:
過程有點小坎坷, 不過還是套用之前 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 快速上手:
- 全局可執行文件:
composer global require phpunit/phpunit
- 項目中添加依賴:
composer require phpunit/phpunit --dev
- 添加
phpunit.xml
配置文件: 用一個現有文件作為模板改改就好 - 編寫測試用例: 一般添加 unit test 作為功能測試(類, 函數 等), feature test 作為 api 接口測試
寫在最后
行文至此, 頗有種 酣暢淋漓 之感.
也許這就是對自己所做的事產生的自豪感吧, 謙虛點是自鳴得意, 吹起牛皮來就是獨孤求敗了.