使用hyperf結合DorisDB數倉的專案實戰體驗

賣蛋餅等你下課發表於2022-05-26

專案背景

做的是一個醫療資料分析專案,在此之前專案其實已經開發完成並且執行一段時間了,但是因為專案的體驗極差以及對開發人員很不友好,與領導商議對專案進行重構

專案邏輯

這個專案完全是一個分析性專案,大致的邏輯就是從病歷庫中計算資料到當前專案庫中,然後從當前庫查詢展示到前臺,主要涉及到資料排名、查詢。
資料量:百萬級

技術選型

框架:本來打算用的springboot進行重構,因為公司整體的專案都在向java語言轉移。但是因為當時業務部門對時間卡的很緊,考慮到這個專案跟語言的關係也不是很大,決定使用hyperf進行快速開發。
(因為以前的版本是用的laravel框架,可以很效率的轉移到hyperf)

資料庫:之前用的是mysql,但是計算和查詢的速度都是不盡人意。因為不是直接的業務資料庫,於是決定選用OLAP型資料庫搭建數倉。最後在clickhouse和Dorisdb中選擇了Dorisdb

數倉搭建

當前專案的計算資料全部基於病歷資料庫,所以我們要對病歷資料庫建一個數倉,然後在這個基礎上新建幾張計算結果的表就可以了,我們平臺業務的要求是月更一次,所以對資料實時性的要求很低,但是後面上線之後我們改成了T+1更新模式。

資料同步

因為對實時性沒要求,所以採用的是kettle定時指令碼的模式,這裡不對kettle做過多介紹,其他類似產品還有datax、canal

數倉使用

數倉的具體使用每個產品也都有自己的特點,不做過多闡述。這裡具體說下Dorisdb,他本身的語法和mysql極度相似,所以好多sql都是通用的,像laravel、hyperf這些框架的model也是直接支援使用的。這也是當時選用這個資料庫的原因之一。其他單獨特性自行去官網瞭解

計算資料

到這一步才算把swoole的優勢發揮出來,dorisdb本身也比mysql快,但是框架也起到了很大的作用
直接貼結果,前後計算時間對比:

60w病歷資料(彙總欄位500個左右) + 100w+隨訪資料時間參考:

老版(laravel + mysql) 50小時左右

新版(hyperf + dorisdb) 開啟多協程(50個) 40分鐘左右

新版(hyperf + dorisdb) 一個協程 5個小時

訪問速度

在老版中最慢的一個介面需要2min,在更新後介面都能達到秒級

ab測試結果因為老專案停運了當時結果也沒儲存,所以就不貼出來了

訪問速度整體看來,我認為作用是數倉 > 框架處理的,因為之前介面慢的主要原因其實就是在於資料庫層面

注意事項

hyperf框架日常使用和laravel基本沒有太大區別,只是在執行的時候是基於一個程式的。所以直接改動程式碼可能不會像fpm框架那樣直接看到效果,推薦使用官方推薦的熱部署

輪子方面確實與laravel還有很大差距,目前基於hyperf的擴充還是很少的,但是有很多擴充在兩種框架都是通用的

中同一個客戶端的請求都是單例的,所以你不能直接更改request資料,這個問題我當時鬱悶了好久

如果有個跑資料指令碼,想啟用多個協程,一定要控制協程的數量,比如我跑資料開啟的是50個協程,那麼一旦數量達到50個後,就要等全部協程工作完成後再開啟下一輪的50個協程。這個具體的協程數量可以根據資料庫配置和伺服器配置自己定義

協程管理可以用WaitGroup來控制,用法也很簡單,swoole官方文件和hyperf文件上都有介紹

貼一段跑資料的demo

$wg = new WaitGroup();
RedisUtil::set($this->redisKey, 0);
foreach ($provinces as $province) {
    for ($year = 2017; $year <= date('Y'); $year++) {
        $cities = $provincesData[$province];
        foreach ($cities as $city) {
            // 如果協程數量達到50  掛起等待執行完成後開啟新協程
            if ($wg->count() >= 50) {
                $wg->wait();
            }
            $wg->add();
            // 具體業務程式碼

            RedisUtil::incrBy($this->redisKey, count($all));
            SyncLog::updateLog($log_id, RedisUtil::get($this->redisKey), 0);
            $this->output->writeln("同步數量:".RedisUtil::get($this->redisKey));
            $wg->done();
        }

    }
}

部署

傳統的php專案配合nginx轉發一下目錄可能就ok了,但是swoole的http server是基於一個程式的,所以可以用nginx做一個反向代理。

因為有程式的原因,還是使用docker部署,不然還要守護這個程式,而且官方的dockerfile也為我們準備了最合適的php版本,不用額外去裝一些擴充。

關於專案重啟期間訪問失敗的問題,如果一定要專案不能停止,可以將專案分兩個埠部署兩次用nginx做負載均衡即可

這裡貼出dockerfile和部署指令碼

# Default Dockerfile
#
# @link     https://www.hyperf.io
# @document https://hyperf.wiki
# @contact  group@hyperf.io
# @license  https://github.com/hyperf-cloud/hyperf/blob/master/LICENSE

FROM hyperf/hyperf:7.4-alpine-v3.11-swoole
LABEL maintainer="Hyperf Developers <group@hyperf.io>" version="1.0" license="MIT" app.name="Hyperf"

##
# ---------- env settings ----------
##
# --build-arg timezone=Asia/Shanghai
ARG timezone

ENV TIMEZONE=${timezone:-"Asia/Shanghai"} \
    #APP_ENV=prod \
    SCAN_CACHEABLE=(true)

# update
RUN set -ex \
    # show php version and extensions
    && php -v \
    && php -m \
    && php --ri swoole \
    #  ---------- some config ----------
    && cd /etc/php7 \
    # - config PHP
    && { \
        echo "upload_max_filesize=128M"; \
        echo "post_max_size=128M"; \
        echo "memory_limit=-1"; \
        echo "date.timezone=${TIMEZONE}"; \
    } | tee conf.d/99_overrides.ini \
    # - config timezone
    && ln -sf /usr/share/zoneinfo/${TIMEZONE} /etc/localtime \
    && echo "${TIMEZONE}" > /etc/timezone \
    # ---------- clear works ----------
    && rm -rf /var/cache/apk/* /tmp/* /usr/share/man \
    && echo -e "\033[42;37m Build Completed :).\033[0m\n"

WORKDIR /opt/www

# Composer Cache
# COPY ./composer.* /opt/www/
# RUN composer install --no-dev --no-scripts

COPY . /opt/www
RUN  composer install --no-dev -o && php bin/hyperf.php

EXPOSE 9501

ENTRYPOINT ["php", "/opt/www/bin/hyperf.php", "start"]
# 容器名
containerName='你的專案名'
cid=$(docker ps | grep $containerName | awk '{print $1}')
if [ -n "$cid" ]
then
    echo "prepare to stop $cid"
    docker stop $cid
    echo "stop $cid success"
    echo "prepare to remove $cid"
    docker rm $cid
    echo "stop $cid success"
else
    echo "no such container"
fi
# 映象名
oldBuild="qc_doris"
oldImageID=$(docker images | grep $oldBuild | awk '{print $3}')
if [ -n "$oldImageID" ]
then
    echo "prepare to remove old image"
    docker rmi $oldImageID
    echo "remove $oldImageID success"
else
    echo "no such image"
fi
docker build -t qc_doris:0.1 .
echo "build success"
#run
docker run -d -v $(pwd)/runtime/logs:/opt/www/runtime/logs \
  -v $(pwd)/.env:/opt/www/.env \
  -v $(pwd)/static:/opt/www/static \
  -p 9501:9501 --privileged=true  --name xxx xxx:0.1
echo "running success"
# -d:後臺執行;  -p:將容器暴露埠對映到伺服器埠     imageName

總結

如果從laravel過渡到hyperf整體還是十分絲滑的,甚至你不怎麼了解swoole也可以直接使用,但是還是推薦大家把swoole官方的文件啃一遍,有助於理解框架的一些設計。使用體驗目前還不錯

專案整體重構時間加上數倉建設其實只用了一個月,去年年底上線到現在已經穩定執行了有一段時間。這次重構讓業務部門很滿意,也讓領導很滿意。這裡推薦大家去擁抱新的技術但是同時也不要為了技術而技術。一個專案開發不僅僅是IT部門的工作還要與業務有各種磨合,如果有些吃力不討好的技術非要上我覺得是完全沒有必要的。

公司因為整體專案都在用java重構,下次有時間我會講講如何將php專案用java進行重構以及遇到的各種問題和解決辦法。

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章