简介
欢迎来到 Kaxiluo's Blog!
这里是个人技术博客,记录开发过程中遇到的问题和解决方案,涵盖 Web 开发、数据库等技术领域。
博客同步发布于 www.kxler.com。
2022 年文章
记录 2022 年的技术文章,涵盖 WordPress、MySQL、Redis、FastAPI 等技术领域。
WordPress作为全球最流行的博客系统,拥有丰富的主题和插件,是搭建博客的不二选择。下面将开始介绍从零搭建个人博客的步骤。
技术选型
- debian:版本号buster,由于华为云的公共镜还没出debian11,目前只有采用debian10
- nginx:当前最新稳定版1.22.0
- php:版本7.4,考虑到插件可能有php8不兼容,最好还是php7.4
- mysql:版本5.7,mysql8采用了新的认证插件,可能旧的客户端驱动不兼容,还是选择了5.7
前置操作
-
购买云服务器,这里我选择了华为云
-
购买一个域名,我的域名是 www.kxler.com
-
打开terminal或powershell或其他客户端工具登录远程服务器
ssh root@your ip
安装基础软件
如果采用的是华为云服务器,替换源镜像地址和设置时区可以忽略。因为华为云服务器默认使用了华为云debian镜像,并且设置了上海时区。
# 使用阿里云debian镜像
cp /etc/apt/sources.list /etc/apt/sources.list.bak \
&& sed -i -E 's/(deb|security).debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list
# 设置时区
ln -snf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && echo 'Asia/Shanghai' > /etc/timezone
# 常规操作,更新和升级软件库
apt update -y && apt upgrade -y
# 安装常用工具
apt install -y vim git wget curl \
&& apt install -y apt-utils telnet net-tools procps iputils-ping lsb-release
安装Nginx
# 添加nginx apt存储库
# 也可以使用apt-key add添加源,不过debian11将要废弃
echo "deb http://nginx.org/packages/debian/ $(lsb_release -sc) nginx" >> /etc/apt/sources.list \
&& echo "deb-src http://nginx.org/packages/debian/ $(lsb_release -sc) nginx" >> /etc/apt/sources.list
apt install -y gnupg2 \
&& curl -s https://nginx.org/keys/nginx_signing.key | gpg --no-default-keyring --keyring gnupg-ring:/etc/apt/trusted.gpg.d/nginx.gpg --import \
&& chown _apt /etc/apt/trusted.gpg.d/nginx.gpg
# 安装
apt update -y
# 如果此时使用 apt search ^nginx$ 可以看到nginx已经是最新的稳定版本了
apt install nginx
# 确认安装成功
nginx -v
安装PHP
目前debian11的默认php版本7.4,而debian10默认php版本7.3。我选择的debian10,这里需要添加php apt储存库。
# 添加php apt存储库
echo "deb https://packages.sury.org/php/ $(lsb_release -sc) main" | tee /etc/apt/sources.list.d/php.list
wget -O /etc/apt/trusted.gpg.d/php.gpg https://packages.sury.org/php/apt.gpg
apt update -y
# 此时使用 apt search php7.4 确认已有php7.4版本
apt install -y php7.4 php7.4-fpm
# 确认安装成功
php -v
php-fpm7.4 -v
安装MySQL
# 下载包
cd /tmp && wget https://dev.mysql.com/get/Downloads/MySQL-5.7/mysql-server_5.7.39-1debian10_amd64.deb-bundle.tar
# 解压
tar -xvf mysql-server_5.7.39-1debian10_amd64.deb-bundle.tar
# 安装依赖
apt install -y libaio1 libatomic1 libmecab2 libnuma1 psmisc libncurses6
# 依次安装,期间有提示输入和确认密码
dpkg -i mysql-common_5.7.39-1debian10_amd64.deb \
&& dpkg -i mysql-community-client_5.7.39-1debian10_amd64.deb \
&& dpkg -i mysql-client_5.7.39-1debian10_amd64.deb \
&& dpkg -i mysql-community-server_5.7.39-1debian10_amd64.deb \
&& dpkg -i mysql-server_5.7.39-1debian10_amd64.deb
# 配置文件 /etc/mysql/mysql.conf.d/mysqld.cnf
启动服务
# 启动 nginx php-fpm mysql
service nginx start
service php7.4-fpm start
service mysql start
# 使用 ps aux 查看进程是都启动
安装wordpress源码
这里将wordpress源码安装到/workspace/wordpress
cd /tmp && wget https://cn.wordpress.org/latest-zh_CN.tar.gz \
&& mkdir /workspace \
&& tar -xvf latest-zh_CN.tar.gz -C /workspace
配置
配置nginx server
调整nginx.conf如下:
/etc/nginx/nginx.conf
#user nginx; user www-data; worker_processes auto; error_log /var/log/nginx/error.log notice; pid /var/run/nginx.pid;
events { worker_connections 1024; }
http { include /etc/nginx/mime.types; default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for"'; #long time #check_shm_size 5M; # Allow the server to close the connection after a client stops responding. reset_timedout_connection on; client_header_timeout 15; # Send the client a "request timed out" if the body is not loaded by this time. client_body_timeout 10; # If the client stops reading data, free up the stale client connection after this much time. send_timeout 15; # Timeout for keep-alive connections. Server will close connections after this time. keepalive_timeout 30; # Number of requests a client can make over the keep-alive connection. keepalive_requests 30; client_body_buffer_size 128k; client_max_body_size 10m; proxy_read_timeout 180s; # Compression. gzip on; gzip_min_length 10240; gzip_proxied expired no-cache no-store private auth; gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml; gzip_disable "msie6"; # Sendfile copies data between one FD and other from within the kernel. sendfile on; # Don't buffer data-sends (disable Nagle algorithm). tcp_nodelay on; # Causes nginx to attempt to send its HTTP response head in one packet, instead of using partial frames. tcp_nopush on; # Hide web server information # server_tokens off; # server_info off; # server_tag off; # redirect server error pages to the static page error_page 404 /404.html; error_page 500 502 503 504 /50x.html; include /etc/nginx/conf.d/*.conf;}
新增server配置如下:
/etc/nginx/conf.d/wordpress.conf
server { listen 80 default_server; listen [::]:80 default_server ipv6only=on; server_name kxler.com www.kxler.com; root /workspace/wordpress; add_header X-Frame-Options "SAMEORIGIN"; add_header X-XSS-Protection "1; mode=block"; add_header X-Content-Type-Options "nosniff"; index index.html index.htm index.php; charset utf-8; fastcgi_connect_timeout 300; fastcgi_read_timeout 300; fastcgi_send_timeout 300; location = /favicon.ico { access_log off; log_not_found off; } location = /robots.txt { access_log off; log_not_found off; } error_page 404 /index.php; location / { proxy_connect_timeout 90; proxy_send_timeout 90; proxy_read_timeout 90; try_files $uri $uri/ /index.php?$query_string; } location ~ \.php$ { # 取决于配置文件/etc/php/7.4/fpm/pool.d/www.conf中listen是unix socket还是tcp # fastcgi_pass 127.0.0.1:9000; fastcgi_pass unix:/run/php/php7.4-fpm.sock; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; } location ~ .*\.(gif|jpg|jpeg|png|bmp|swf|mp3|mp4|ico|woff|woff2|ttf)$ { expires 30d; log_not_found off; access_log off; } location ~ .*\.(js|css)?$ { expires 12h; access_log off; } location ~ /\.(?!well-known).* { deny all; } access_log /var/log/nginx/access_wordpress-blog.log; error_log /var/log/nginx/error_wordpress-blog.log error;}
检查&重启
# 检查配置是否正确
nginx -t
# 重启nginx
nginx -s reload
MySQL建库、新增用户并授权
使用之前的输入的密码登录mysql root账户
mysql -uroot -p
执行以下sql创建数据库和用户
CREATE DATABASE wordpress DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci;
# 新增用户,用户名wordpress 密码123456。请自行替换成更加复杂的密码
CREATE USER 'wordpress'@'%' IDENTIFIED BY "123456";
GRANT ALL ON wordpress.* TO 'wordpress'@'%';
准备就绪,开始访问网站,下面记录首次访问网站报的错与解决方法
踩坑和埋坑
我申请的域名备案此时还没通过审核,无法用域名访问我的网站,这里直接访问云服务器的公网IP(由于前面nginx配置中已经将wordpress设置为了默认server)
网站报错502
查看nginx日志发现 connect() to unix:/run/php/php7.4-fpm.sock failed (13: Permission denied)。原来php-fpm默认配置listen采用unix socket通信,默认用户是www-data,创建出来的的sock文件身份是www-data,而nginx默认用户是nginx,造成权限问题。
解决:
将/etc/nginx/nginx.conf的user改为www-data (前面给出的nginx配置已改)
再重启nginx:nginx -s reload
网站提示:您的PHP似乎没有安装运行WordPress所必需的MySQL扩展
# 安装php扩展mysqli gd,额外安装wordpress官方推荐的扩展imagick curl zip dom intl
apt install -y php7.4-mysqli php7.4-gd php7.4-mbstring php7.4-curl php7.4-imagick php7.4-zip php7.4-dom php7.4-intl
# 重启php-fpm
service php7.4-fpm restart
网站提示:无法写入wp-config.php文件
一看就是权限问题,将整个wordpress源码授予用户www-data
chown -R www-data:www-data /workspace/wordpress
调整上传最大尺寸配置
php配置 /etc/php/7.4/fpm/php.ini,将默认的文件上传尺寸2M调高到8M
upload_max_filesize = 8M
nginx配置 /etc/nginx/nginx.conf(前面给出的nginx配置已改)
client_max_body_size 8m;
一切就绪
-
访问网站IP,跟着wordpress安装向导,填写数据库连接配置、网站基本配置。
-
接下来就是选择主题,安装插件。
-
大约过了10天,域名备案过审后,此时在云服务商后台将域名解析到云服务器IP,再到wordpress后台
设置>常规修改站点URL配置。 -
访问网站 www.kxler.com ,享受吧!
额外的
头像不显示? wordpress头像地址默认是Gravatar,国内被qiang了,写个插件将其替换为国内镜像地址。 参考自定义方法插件
最近我的博客上线了,接下里我准备给它安装ssl证书。此外,我在网站的访问日志中,发现了大量恶意的请求,主要是尝试登录、尝试执行恶意脚本、获取敏感信息等。虽然WordPress核心总体来说已经足够安全,但是我还是决定禁用xmlrpc,隐藏后台登录入口,以提高网站的安全性。
安装免费的ssl证书-Let's Encrypt
Certbot是一个开源免费的工具,主要功能是为网站自动安装基于Let’s Encrypt服务的SSL证书。
- 安装Certbot
apt install certbot
apt install python3-certbot-nginx
- 验证网站所有权,生成证书文件
certbot certonly --nginx
# 或者让Certbot自动编辑您的nginx配置
# certbot --nginx
默认生成证书的目录 /etc/letsencrypt/live/$domain/
- 编辑nginx配置,手动配置证书
server {
listen 80;
server_name kxler.com www.kxler.com;
# http强制到https
return 301 https://$host$request_uri;
}
server {
listen 443 ssl default_server;
server_name kxler.com www.xkler.com;
root /workspace/wordpress;
# ssl证书
ssl_certificate /etc/letsencrypt/live/kxler.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/kxler.com/privkey.pem;
ssl_session_timeout 30m;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3;
ssl_session_cache shared:SSL:10m;
ssl_ciphers 'ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS';
#以下省略...
}
重启nginx
# 测试配置是否正确
nginx -t
# 重启nginx
nginx -s reload
此时网站后台无法使用https域名登录,需要使用IP或http登录到wordpress后台,在设置>常规修改站点URL配置为https地址https://www.kxler.com。
- 关于自动续订
Let's Encrypt颁发的是短期证书(90 天),需要3个月内至少续订一次证书。
大多数Certbot安装都带有开箱即用的自动续订功能,Cerbot使用 certbot renew 完成自动续订,生成的续订定时任务在 /etc/cron.d/certbot。
如果您仍不确定,可以按照官网手动配置自动续订。 Certbot自动续订
禁用xmlrpc.php
最近查看nginx访问日志,发现了大量关于xmlrpc.php的恶意请求。经查资料得知,XML-RPC是支持WordPress与其他系统之间通信的规范,如今已被REST API取代。所以决定禁用它。
禁用方式有很多种,这里选择修改nginx配置文件,在wordpress.conf配置中添加
# 务必加在 location ~ \.php${...} 前面的位置
location ~* ^/xmlrpc.php$ {
return 403;
}
重启nginx
# 测试配置是否正确
nginx -t
# 重启nginx
nginx -s reload
隐藏登录入口
此外,在nginx访问日志,也发现了大量尝试登录的请求,这里直接修改登录入口,以提高安全性。
首先安装插件WPS Hide Login,然后在后台配置登录入口。
另外,如果觉得有必要还可安装限制登录频率的插件。
在日常工作中,我们可能会遇到根据数据存在/不存在,再决定是否插入数据的场景。例如不存在则插入数据,存在则更新数据(或不变)。然而在高并发情况下,可能会重复插入或因唯一约束导致的报错,下面给出写数据时的处理方案。
示例:每天生成一张背景图,创建背景图表,在date字段创建唯一索引。
CREATE TABLE `backgrounds` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`date` date NOT NULL COMMENT '日期',
`url` varchar(255) NOT NULL DEFAULT '' COMMENT '地址',
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`),
UNIQUE KEY `udx_date` (`date`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
对于存在的定义:主键或UNIQUE KEY在表中已存在
不存在插入,存在不变
INSERT IGNORE INTO
不存在时:影响1行; 存在时:影响0行;
INSERT IGNORE INTO backgrounds(`date`, `url`)
VALUES('2022-08-18', 'https://1.png');
INSERT INTO NOT EXISTS
不存在时:影响1行; 存在时:影响0行;
INSERT INTO backgrounds(`date`, `url`)
SELECT '2022-08-18' as `date`, 'https://1.png' as `url` FROM backgrounds
WHERE NOT EXISTS (SELECT 1 FROM backgrounds WHERE `date` = '2022-08-18');
不存在插入,存在则更新
ON DUPLICATE KEY UPDATE
不存在时:影响1行; 存在时:若新增数据与原数据有变化影响2行,mysql内部先执行了delete,然后再insert;
INSERT INTO backgrounds(`date`, `url`)
VALUES('2022-08-19', 'https://123.png')
ON DUPLICATE KEY UPDATE `url` = 'https://123.png';
REPLACE INTO
不存在时:影响1行; 存在时:影响2行,mysql内部先执行了delete,然后再insert;
REPLACE INTO backgrounds(`date`, `url`)
VALUES('2022-08-18', 'https://1.png');
其他方案
-
插入数据时,锁定表的读写
LOCK TABLES,但是会降低并发性。 -
根据Redis集合元素的唯一性判断是否存在。
-
借助外部锁,例如Redis锁,插入时获取锁。
最近在工作中有一个统计图片浏览量和保存次数的需求,由于性能问题,我们不能频繁的操作MySQL数据库,在这里就可以使用redis作为计数器,再定期更新到数据库。下面我总结了几种常见的PV、UV、IP统计方案。
- PV(Page View)页面浏览量
- UV(Unique Visitor)独立访客数量
- IP(Internet Protocol)独立IP数
Redis INCR
简单的key-value计数器,key包含要统计对象的标识,value为访问量。
命令介绍:
INCR:将key值递增1,返回最新的值。
INCR是原子增量,即使多个客户端针对同一key发出 INCR,也永远不会进入争用状态。例如,客户端 1 读取“10”,客户端 2 同时读取“10”,两者都递增为 11,并将新值设置为 11,这种情况永远不会发生。最终值将始终为 12。
使用示例:
# 新增文章123的访问量
>incr article:123:view
1
>incr article:123:view
2
# 获取文章123的访问量
>get article:123:view
2
适用于:
PV、无需去重的统计
Redis Sets
redis集合元素具有唯一性,可以轻松实现去重统计。
命令介绍:
SADD:将新成员添加到集合中。SREM:从集合中删除指定的成员。SISMEMBER:判断元素是否是集合的成员。SINTER:返回两个或多个集合的交集。SCARD:返回集合的大小(又名基数)。
使用示例:
# 文章123被用户2访问
>sadd article:123:user 2
1
# 文章123被用户6访问
>sadd article:123:user 6
1
# 检查用户2是否浏览了文章123
>sismember article:123:user 2
1
# 文章123的总用户访问量
>scard article:123:user
2
假设每个文章需要存储100w个用户id(id依次递增1-1000000),那么内存占用量为: 6543216 bytes = 6543216/1024/1024 MB = 6.24 MB。 单个对象需要的内存不多,但是如果有1000个对象(文章),那么存储量将是 6.24 * 1000 = 6240 MB,这就很离谱了。
适用于:
少量数据、精确的统计、独立IP数
性能:
大多数集合操作(包括添加、删除和检查项目是否为集合成员)都是 O(1)。这意味着它们非常高效。但是,对于具有数十万或更多成员的大型集合,在运行SMEMBERS命令时应格外小心。此命令为 O(n),并在单个响应中返回整个集合。作为替代方法,请考虑SSCAN,它允许您以迭代方式检索集合的所有成员。
对大型数据集的集合成员资格检查可能会占用大量内存。如果您担心内存使用情况并且不需要完美的精度,请考虑使用Bloom过滤器或Cuckoo过滤器作为集合的替代方案。
Redis Bitmap
位图,用一个bit位来表示某个元素对应的状态(0或1)。
位图的最大优点之一是,在存储信息时,它们通常可以节省极大的空间。仅使用512MB 内存即可记住40亿用户的布尔信息(例如,知道用户是否想要接收新闻稿)。
命令介绍:
SETBIT:对key所储存的字符串值,设置或清除指定偏移量上的位(bit)。返回值为偏移位上的原始值。
具体语法:setbit key offset value,其中offset是偏移位,通常可以是对象标识ID;value表示对偏移位清除或设置,即0或1。
-
BITCOUNT:返回key所储存的字符串值设置为 1 的位数。 -
GETBIT:获取指定偏移量上的位值(0或1)。 -
BITOP operation destkey key [key ...]:在多个字符串key之间执行按位运算(AND、OR、XOR 和 NOT),并将结果存储在目标键中。 -
BITPOS key bit [start [end [BYTE | BIT]]]:返回字符串中设置为 1 或 0 的第一个位的位置。
使用示例:
# 文章123被用户6访问
>setbit article:123:user 6 1
0
# 文章123被用户9访问
>setbit article:123:user 9 1
0
>setbit article:123:user 9 1
1
# 检查用户9是否浏览了文章123
>getbit article:123:user 9
1
# 文章123的总用户访问量
>bitcount article:123:user
2
假设每个文章需要存储100w个用户id,占用的内存是 1000000/8/1024/1024=0.11920929 MB。 如果有1000个文章,那么存储量将是 0.11920929 * 1000 = 119.20929 MB。 由此可见,当统计较多对象时还是比较消耗内存的。
适用于:
集合成员对应整数0-n、精确统计、 统计对象key单一或较少、用户月签到记录等
Redis HyperLogLog
HyperLogLog是用来做基数统计的算法。它的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定的、并且是很小的。
Redis HyperLogLog 牺牲数据的精准性来节省内存的占用空间,只需要12K内存就能统计2^64个数据, 0.81% 的标准误差。
命令介绍:
PFADD:将项目添加到 HyperLogLog 中。PFCOUNT:返回集合中项目数的估计值。PFMERGE:将两个或多个HyperLogsLogs合并为一个。
使用示例:
# 文章123被用户3访问
>pfadd article:123:view 3
1
# 文章123被用户95访问
>pfadd article:123:view 95
1
# 被添加元素可以是字符串
>pfadd article:123:view abc
1
# 获取访问量
>pfcount article:123:view
3
# 批量添加元素
>pfadd hll a b c d e f g
1
>pfcount hll
7
同步策略
-
在业务低频的时候同步到数据库。
-
对于实时性要求高的,后台可以直接读取redis的数据。
-
在key加上日期,每次持久化只处理过去时间的、key的值不会再变化的,如此一来,可以保证临界点数据(读出redis数据,然后写入数据库,最后重置或删除key期间产生的新数据)不丢失。
想想埋点,会不会是更好地方案呢~
如果我们想在Markdown中展示文件夹的树状格式,可以使用Windows自带的tree命令输出文件树,然而自带的tree命令不支持排除指定目录等高级功能。这时候就需要安装一个加强版的tree命令 - Tree for Windows,它提供了排除目录、排序规则等丰富的参数。
先来看看Windows自带的tree命令用法
# 查看使用说明
PS C:\Users\Administrator> tree /?
TREE [drive:][path] [/F] [/A]
/F 显示每个文件夹中文件的名称。
/A 使用 ASCII 字符,而不使用扩展字符。
自带的tree命令十分简单,显然不能满足我们排除目录的需求,此时我们就需要安装强化版的tree命令。
使用强化版的Tree命令 - Tree for Windows
下载安装
-
选择 Binaries ZIP 下载
-
解压,找到bin目录下的可执行文件
tree.exe,复制到任意目录(我常常将它们放在C:\bin)
强化版Tree命令的使用说明
# 在tree.exe所在目录执行查看使用说明的命令
> ./tree.exe --help
usage: tree [-adfghilnpqrstuvxACDFNS] [-H baseHREF] [-T title ] [-L level [-R]]
[-P pattern] [-I pattern] [-o filename] [--version] [--help] [--inodes]
[--device] [--noreport] [--nolinks] [--dirsfirst] [--charset charset]
[--filelimit #] [<directory list>]
-a All files are listed.
-d List directories only.
-l Follow symbolic links like directories.
-f Print the full path prefix for each file.
-i Don\'t print indentation lines.
-q Print non-printable characters as '?'.
-N Print non-printable characters as is.
-p Print the protections for each file.
-u Displays file owner or UID number.
-g Displays file group owner or GID number.
-s Print the size in bytes of each file.
-h Print the size in a more human readable way.
-D Print the date of last modification.
-F Appends '/', '=', '*', or '|' as per ls -F.
-v Sort files alphanumerically by version.
-r Sort files in reverse alphanumeric order.
-t Sort files by last modification time.
-x Stay on current filesystem only.
-L level Descend only level directories deep.
-A Print ANSI lines graphic indentation lines.
-S Print with ASCII graphics indentation lines.
-n Turn colorization off always (-C overrides).
-C Turn colorization on always.
-P pattern List only those files that match the pattern given.
-I pattern Do not list files that match the given pattern.
-H baseHREF Prints out HTML format with baseHREF as top directory.
-T string Replace the default HTML title and H1 header with string.
-R Rerun tree when max dir level reached.
-o file Output to file instead of stdout.
--inodes Print inode number of each file.
--device Print device ID number to which each file belongs.
--noreport Turn off file/directory count at end of tree listing.
--nolinks Turn off hyperlinks in HTML output.
--dirsfirst List directories before files.
--charset X Use charset X for HTML and indentation line output.
--filelimit # Do not descend dirs with more than # files in them.
使用例子
查看目录E:\testdir下的文件树,排除目录cachedir,输出排序规则以目录优先。
> ./tree.exe E:\testdir\ -I cachedir --dirsfirst
E:\testdir\
|-- app
| |-- code
| | `-- test.txt
| |-- emptydir
| `-- index.txt
|-- bar
| `-- 233.txt
|-- 1.txt
|-- 2.txt
`-- a.txt
这是一个开箱即用的FastAPI脚手架,集成了ORM模型、JWT认证、日志系统、异常处理、路由注册、系统配置、调度任务等常用的模块。
FastAPI是一个用于构建API的现代、高性能的Python web框架,由于它只提供app核心,因此需要我们自己组织项目结构,通常得编写一些Web API项目必备的功能模块。对此,我在github上搜索相关的最佳实践,然而没有找到心仪的脚手架,所以决定封装一个FastAPI骨架项目。
设计思想
- 层级结构清晰
- 简洁优雅
- 易于扩展
- 开箱即用
项目结构
源码GitHub地址 -> kaxiluo/fastapi-skeleton
/kaxiluo/fastapi-skeleton/
|-- app
| |-- commands ----- 放置一些命令行
| | `-- __init__.py
| |-- exceptions ----- 自定义的异常类
| | |-- __init__.py
| | `-- exception.py
| |-- http ----- http目录
| | |-- api ----- api控制器目录
| | | |-- __init__.py
| | | |-- auth.py ----- 登录认证api的控制器
| | | |-- demo.py
| | | `-- users.py
| | |-- middleware ----- 放置自定义中间件
| | | `-- __init__.py
| | |-- __init__.py
| | `-- deps.py ----- 依赖
| |-- jobs ----- 调度任务
| | |-- __init__.py
| | `-- demo_job.py
| |-- models ----- 模型目录
| | |-- __init__.py
| | |-- base_model.py ----- 定义模型的基类
| | `-- user.py
| |-- providers ----- 核心服务提供者
| | |-- __init__.py
| | |-- app_provider.py ----- 注册应用的全局事件、中间件等
| | |-- database.py ----- 数据库连接
| | |-- handle_exception.py ----- 异常处理器
| | |-- logging_provider.py ----- 集成loguru日志系统
| | `-- route_provider.py ----- 注册路由文件routes/*
| |-- schemas ----- 数据模型,负责请求和响应资源数据的定义和格式转换
| | |-- __init__.py
| | `-- user.py
| |-- services ----- 服务层,业务逻辑层
| | |-- auth ----- 认证相关服务
| | | |-- __init__.py
| | | |-- grant.py ----- 认证核心类
| | | |-- hashing.py
| | | |-- jwt_helper.py
| | | |-- oauth2_schema.py
| | | `-- random_code_verifier.py
| | `-- __init__.py
| |-- support ----- 公共方法
| | |-- __init__.py
| | `-- helper.py
| `-- __init__.py
|-- bootstrap ----- 启动项
| |-- __init__.py
| |-- application.py ----- 创建app实例
| `-- scheduler.py ----- 创建调度器实例
|-- config ----- 配置目录
| |-- auth.py ----- 认证-JWT配置
| |-- config.py ----- app配置
| |-- database.py ----- 数据库配置
| `-- logging.py ----- 日志配置
|-- database
| `-- migrations ----- 初始化SQL
| `-- 2022_09_07_create_users_table.sql
|-- routes ----- 路由目录
| |-- __init__.py
| `-- api.py ----- api路由
|-- storage
| `-- logs ----- 日志目录
|-- README.md
|-- main.py ----- app/api启动入口
|-- requirements.txt
`-- scheduler.py ----- 调度任务启动入口
集成的模块
- 日志系统
集成 loguru,一个优雅、简洁的日志库
- 异常处理
定义认证异常类,注册 Exception Handler
- 路由注册
路由集中注册,按模块划分为不同的文件,代码层次结构清晰
- 系统配置
基于 pydantic.BaseSettings,使用 .env 文件设置环境变量。配置文件按功能模块划分,默认定义了app基础配置、数据库配置(mysql+redis)、日志配置、认证配置
- 数据库 ORM模型
基于 peewee,一个轻量级的Python ORM框架
- 中间件
默认注册了全局CORS中间件
- JWT认证
默认提供了账号密码和手机号验证码两种认证方式。框架易于扩展新的认证方式。
测试登录认证请先执行初始化的SQL:fastapi-skeleton/database/migrations/*.sql
注:验证码的存储依赖redis
- 调度任务
基于 APScheduler 调度任务框架
注:定时任务与api是分开启动的
运行
-
执行初始化SQL:
/database/migrations/2022_09_07_create_users_table.sql -
API
uvicorn main:app --host 0.0.0.0 --port 8080
- 调度器
python scheduler.py
关于部署部分,参见我的另一篇文章 fastapi部署
参考
FastAPI作者的全栈项目脚手架 full-stack-fastapi-postgresql
代码结构组织风格参考 Laravel框架
分享一个工作中使用dmn-js的实践案例。需求背景是在绩效系统中实现量化打分、用户可自定义得分规则。对此我们应用到了DMN,其XML由决策引擎Camunda直接执行,前端方面集成了dmn-js。
考虑到编写规则表和打分功能的易用性,系统管理了规则属性列表,用户在设置规则时,只需选择规则属性,就会自动生成DMN关系图,然后填充相应的规则表达式即可。
- DMN(Decision Model and Notation)决策模型标记
- Camunda 工作流引擎
- dmn-js 实现在浏览器中查看和编辑DMN关系图
本文主要是分享在vue应用中,使用dmn-js实现在浏览器中选择规则属性自动生成对应的DMN关系图骨架和XML。
演示

源码
源码GitHub地址 -> kaxiluo/dmn-js-vue
最近使用自己编写的fastapi的脚手架开发了一个项目,下面分享部署fastapi的过程。
-
venv: Python创建虚拟环境的工具。
-
uvicorn: Python ASGI Web服务器。轻量级,不具备进程监控。
-
gunicorn: 用于UNIX的Python WSGI Web服务器。可以用来管理Uvicorn,充当进程管理器,Gunicorn的功能齐全且成熟。
本教程环境说明:操作系统 debian10; 项目 kaxiluo/fastapi-skeleton
安装python虚拟环境和项目依赖
# 创建python虚拟环境
python3 -m venv venv-fastapi-demo
# 进入虚拟环境
source ./venv-fastapi-demo/bin/activate
# 进入项目根目录
cd /path/to/fastapi-demo/
# 安装项目依赖
pip install -r requirements.txt
# 可能中途报错,提示缺少库或请升级pip
# 升级pip
python -m pip install --upgrade pip
# 安装依赖
apt install build-essential libssl-dev libffi-dev python3-dev
# 再次执行安装项目依赖命令
pip install -r requirements.txt
启动
- 方式一
使用uvicorn启动,一般用于开发环境
uvicorn main:app --host 0.0.0.0 --port 8080
- 方式二
安装gunicorn,并启动
pip install gunicorn
gunicorn main:app --workers 2 --worker-class uvicorn.workers.UvicornWorker --bind 0.0.0.0:8080
这些启动参数也可以用一个配置文件管理,了解更多功能参数请运行 gunicorn -h
- 方式三
以守护进程方式运行gunicorn
nohup gunicorn main:app --workers 2 --worker-class uvicorn.workers.UvicornWorker --bind 0.0.0.0:8080 > /dev/null &
# 或者
# 其实gunicorn提供了-D参数
gunicorn main:app -D --workers 2 --worker-class uvicorn.workers.UvicornWorker --bind 0.0.0.0:8080
停止服务和重启
首先找到gunicorn进程PID,然后通过信号量停止或重启服务
# 查看主进程ID
pstree -ap | grep gunicorn
停止
# 正常停止主进程及其子进程
kill -TERM pid
重启
kill -HUP pid
以上方式都不适合生产环境,下面是终极方案。
加入系统服务运行,配置nginx反向代理,设置开机启动
- 新建gunicorn服务文件
/etc/systemd/system/gunicorn.service:
[Unit]
Description=gunicorn - python http server
After=network.target
[Service]
Type=forking
PIDFile=/var/run/gunicorn.pid
# 项目根目录
WorkingDirectory=/path/to/fastapi-demo
# gunicorn启动命令
ExecStart=/path/to/venv-fastapi-demo/bin/python3 /path/to/venv-fastapi-demo/bin/gunicorn main:app -D --pid /var/run/gunicorn.pid --workers 2 --worker-class uvicorn.workers.UvicornWorker --bind 127.0.0.1:8080
ExecReload=/bin/kill -s HUP $MAINPID
ExecStop=/bin/kill -s TERM $MAINPID
[Install]
WantedBy=multi-user.target
- 加载新的服务配置文件
systemctl daemon-reload
- 配置nginx反向代理
server {
listen 80;
server_name demo.fastapi.com;
rewrite ^(.*)$ https://$host$1 permanent;
}
server {
listen 443 ssl http2;
server_name demo.fastapi.com;;
# ...省略ssl部分...
charset utf-8;
location = /favicon.ico { access_log off; log_not_found off; }
location = /robots.txt { access_log off; log_not_found off; }
location / {
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://127.0.0.1:8080;
}
location ~ /\.(?!well-known).* {
deny all;
}
access_log /var/log/nginx/access_fastapi-demo.log;
error_log /var/log/nginx/error_fastapi-demo.log error;
}
- 启动Gunicorn
service gunicorn start
# 查看服务状态
service gunicorn status
- 重启
# 平滑重启 主进程id不会变
service gunicorn reload
# 重启
service gunicorn restart
- 设置开机启动
systemctl enbale gunicorn
2023 年文章
记录 2023 年的技术文章,涵盖 PHP、Vue、Docker 等技术领域。
设想一下,当需求是生成一个样式排版复杂的Excel文件,如果直接使用开源库的API来实现各种样式,那这也忒难了。好在,我们可以预制好一个Excel模板,再将数据填充到对应的位置,这样一来就只需要关注数据,从而轻松的生成了复杂的Excel文件。
分享一个以前写的Excel模板文件渲染工具,模板中支持定义各种变量、自定义渲染行为、设置回调函数,轻松实现复杂的Excel生成。
演示

演示代码如下:
use Kaxiluo\PhpExcelTemplate\CellVars\CallbackContext;
use Kaxiluo\PhpExcelTemplate\CellVars\CellArray2DVar;
use Kaxiluo\PhpExcelTemplate\CellVars\CellArrayVar;
use Kaxiluo\PhpExcelTemplate\CellVars\RenderDirection;
use Kaxiluo\PhpExcelTemplate\PhpExcelTemplate;
$data = [
['个人任务执行情况', '完成X系统开发', '40', '35', '任务延期一次'],
['技术贡献', '主持基础设施项目建设', '30', '30', ''],
['素质与能力', '认真负责,积极工作', '20', '20', ''],
['防疫工作', '严格执行防疫措施,按要求填写防疫信息统计表', '10', '10', ''],
];
$items = new CellArray2DVar(
$data,
true,
false,
function (CallbackContext $context) use ($data) {
if ($context->getLoopColKey() === 3) {
if ($context->getValue() < $data[$context->getLoopRowKey()][2]) {
$context->getStyle()->getFont()->getColor()->setARGB('FFFF0000');
}
}
}
);
// 模板变量定义
$vars = [
'username' => 'lyy',
'department' => 'IT中心',
'dateRange' => '2022-03-01 - 2022-03-31',
'leader' => 'Tim',
'items' => $items,
'totalScore' => '=SUM(D5:D' . (4 + count($data)) . ')',
'x' => new CellArrayVar(['Peace', 'and', 'Love'], RenderDirection::DOWN, false),
];
// 保存文件
PhpExcelTemplate::save('./example-kpi.xlsx', './example-kpi-output.xlsx', $vars);
// 或者浏览器下载
// PhpExcelTemplate::download('./example-kpi.xlsx', 'example-kpi-output.xlsx', $vars);
安装
使用composer安装
composer require kaxiluo/php-excel-template
GitHub地址 -> kaxiluo/php-excel-template
功能点
- 支持字符串变量渲染
- 支持[一维数组变量]渲染,自定义渲染方向(向下的行或向右的列)、是否插入新的行或列
- 支持[[二维数组变量]]渲染,自定义向下是否插入新的行、向右是否插入新的列
- 支持设置回调函数,定制渲染样式或其他特殊行为
模板中定义变量
模板中的变量名只允许特定字符(字母数字-_.)
字符串变量(CellStringVar)
1)在模板中使用 {yourStringVarName} 声明字符串变量
2)用法如下:
use Kaxiluo\PhpExcelTemplate\CellVars\CellStringVar;
use Kaxiluo\PhpExcelTemplate\CellVars\CallbackContext;
use Kaxiluo\PhpExcelTemplate\PhpExcelTemplate;
$vars = [
// 默认
'var1' => 'value1',
'var2' => new CellStringVar('value2'),
// 设置回调
'var3' => new CellStringVar('i was red color', function (CallbackContext $context) {
$context->getStyle()->getFont()->getColor()->setARGB('FFFF0000');
}),
];
PhpExcelTemplate::save('/path/to/templateFile.xlsx', '/path/to/outputFile.xlsx', $vars);
一维数组变量(CellArrayVar)
1)在模板中使用 [yourArrayVarName] 声明一维数组变量
2)用法如下:
use Kaxiluo\PhpExcelTemplate\CellVars\CellArrayVar;
use Kaxiluo\PhpExcelTemplate\CellVars\CallbackContext;
use Kaxiluo\PhpExcelTemplate\CellVars\RenderDirection;
use Kaxiluo\PhpExcelTemplate\PhpExcelTemplate;
$vars = [
// 默认向下渲染,插入新的行
'var1' => ['x1', 'x2'],
'var2' => new CellArrayVar(['x', 'x']),
'var3' => new CellArrayVar(['x', 'x'], RenderDirection::DOWN, true),
// 向右渲染,插入新的列
'var4' => new CellArrayVar(['x', 'x'], RenderDirection::RIGHT),
// 向右渲染,不插入新的列
'var5' => new CellArrayVar(['x', 'x'], RenderDirection::RIGHT, false),
// 向下渲染,不插入新的行
'var6' => new CellArrayVar(['x', 'x'], RenderDirection::DOWN, false),
// 设置回调
'var7' => new CellArrayVar(
['x7-1', 'x7-2', 'x7-3'],
RenderDirection::RIGHT,
true,
function (CallbackContext $context) {
// $context->getWorksheet()
// $context->getValue()
// 设置第二条数据(x7-2)加粗
if ($context->getLoopColKey() === 1) {
$context->getStyle()->getFont()->setBold(true);
}
}
),
];
PhpExcelTemplate::save('/path/to/templateFile.xlsx', '/path/to/outputFile.xlsx', $vars);
二维数组变量(CellArray2DVar)
1)在模板中使用 [[yourArray2DVarName]] 声明二位数组变量
2)用法如下:
use Kaxiluo\PhpExcelTemplate\CellVars\CellArray2DVar;
use Kaxiluo\PhpExcelTemplate\CellVars\CallbackContext;
use Kaxiluo\PhpExcelTemplate\PhpExcelTemplate;
$vars = [
// 默认向右-下方渲染,下方插入新的行,右边不插入新的列
'var1' => [['a1', 'b1'], ['a2', 'b2']],
'var2' => new CellArray2DVar([['c1', 'd1'], ['c2', 'd2']]),
'var3' => new CellArray2DVar([['a5', 'b5'], ['a6', 'b6']], true, false),
// 插入新的行和列
'var4' => new CellArray2DVar([['a5', 'b5'], ['a6', 'b6']], true, true),
// 不插入新的行和列
'var5' => new CellArray2DVar([['a5', 'b5'], ['a6', 'b6']], false, false),
// 不插入新的行,插入新的列
'var6' => new CellArray2DVar([['a5', 'b5'], ['a6', 'b6']], false, true),
// 设置回调
'var7' => new CellArray2DVar(
[['x', '88'], ['y', '59'], ['z', '95']],
true,
false,
function (CallbackContext $context) {
// 第二列小于60则字体标红
if ($context->getLoopColKey() === 1 && $context->getValue() < 60) {
$context->getStyle()->getFont()->getColor()->setARGB('FFFF0000');
}
}
),
];
PhpExcelTemplate::save('/path/to/templateFile.xlsx', '/path/to/outputFile.xlsx', $vars);
其他
如果Excel模板中同一个行上的多个变量,均定义为要插入新的行,本程序会自动处理需要插入的最大行数,不会插入额外的行;列同理。用户需要考虑多个变量均为插入新行或列,其产生的相互影响。
当表格中内容过长、占用多行,十分影响用户体验。如何实现文本自动截断、展开、收起呢?vue-clamp可以轻松实现。
安装 vue-clamp
npm i --save vue-clamp
封装组件,方便在 Element-UI Table 中使用
<template>
<el-table-column :prop="prop" :label="label">
<template slot-scope="scope">
<v-clamp autoresize :max-lines="maxLines">{{scope.row[prop]}}
<template #after="{ toggle, expanded, clamped }">
<el-button plain v-if="expanded || clamped"
@click="toggle"
style="margin-left: 5px"
size="small">{{ expanded ? '隐藏' : '展开' }}</el-button>
</template>
</v-clamp>
</template>
</el-table-column>
</template>
<script>
import VClamp from 'vue-clamp'
export default {
components: {
VClamp
},
name: "table-column-v-clamp",
props: {
prop: String,
label: String,
maxLines: { type: Number, default: 2 }
}
}
</script>
轻松使用,一行搞定
<el-table
:data="dataList"
border
style="width: 100%">
<el-table-column prop="id" label="ID"></el-table-column>
<el-table-column prop="name" label="名字"></el-table-column>
<table-column-v-clamp prop="content" label="内容"></table-column-v-clamp>
</el-table>
在线体验
Table设置固定列后,固定列错位
由于El-Tbale固定列的实现是复制一份table,再隐藏其他列。VClamp组件的展开没能触发另一个Table视图更新,导致了固定列错位,高度未自动计算。
更改数据来驱动视图更新,以解决该问题
具体方法 -> ElementUI Table VueClamp Fixed
关于 vue-clamp
可以选择限制行数与/或最大高度,无需指定行高。
支持在布局变化时自动更新。
支持展开/收起被截断部分内容。
支持自定义截断文本前后内容,并且进行响应式更新。
支持在文本末尾、中间或开始位置进行截断
记录一下多年前我使用Windows Docker作为开发环境,运行基于PHP-FPM的项目性能低下的问题。 开发过程中,通过Bind mounts与容器共享源码目录,结果项目访问速度十分慢,特别是遇到源码文件特别多的项目(例如magento2),速度已经慢得无法忍受。
PHP-FPM(进程管理器):一种传统的PHP运行模式,运行时不同于常驻内存,每次都会加载源码。
Bind mounts(绑定挂载):Docker容器中管理数据文件的一种方式,一般适合开发过程中,容器与主机共享源码。
探寻问题原因
- 借助工具分析性能
利用XHProf(一个轻量级的分层性能测量分析器PHP扩展) + Xhgui(搭建非侵入式监控平台),跟踪到了程序大部分的运行时间消耗在 require_once,该语句的作用是引入php文件。由此确定源码文件的读取是性能瓶颈。
- 绑定挂载导致I/O性能低
再做个对比测试,把源码复制到容器中再访问,果然速度变快了。由此确定绑定挂载导致源码读取速度变慢。 查询资料得知,Docker的绑定挂载通过SMB共享来实现的,再加上跨操作系统的文件转换,从而I/O性能变得极低。
综上,因为源码在windows主机上,通用绑定挂载共享到Docker容器中,文件的I/O性能低下,项目基于php-fpm,每次运行消耗大量时间加载源码,从而导致项目速度变慢。
Docker绑定挂载性能测试
分别以Hyper-V和WSL2作为后端引擎,再结合挂载不同位置和配置,测试读写性能和实际项目表现。
准备工作
- 安装WSL2
参见 安装WSL
因为我之前安装过WSL1,所以我只需要升级Linux发行版到WSL2
# 升级内核
wsl --update
# 升级到wsl2
wsl --set-version Ubuntu-22.04 2
- 测试I/O工具
通过dd命令测试I/O性能
- 测试项目实际表现
一个干净的Laravel8项目,仅输出‘hello world’
结果记录
基于机械硬盘,Windows Docker, Linux Container
- Hyper-V + 源码拷贝到容器中
# 运行容器
> docker run --name test001 -it --rm -p 8000:8000 php:7.4-fpm /bin/bash
# 主机中执行命令,复制源码到容器中
> docker cp E:/laravel8 test001:/var/www/html
# 在容器中开始测试
> cd /var/www/html/laravel8/
# 写性能测试
> dd if=/dev/zero of=test.dat bs=1024 count=100000
100000+0 records in
100000+0 records out
102400000 bytes (102 MB, 98 MiB) copied, 0.153936 s, 665 MB/s
# 读性能测试
> dd if=test.dat of=/dev/null bs=1024 count=100000
100000+0 records in
100000+0 records out
102400000 bytes (102 MB, 98 MiB) copied, 0.0692353 s, 1.5 GB/s
# 启动内置Web服务,测试项目访问速度
> php artisan serve --host=0.0.0.0 --port=8000
PHP 7.4.27 Development Server (http://0.0.0.0:8000) started
# 浏览器中访问 http://127.0.0.1:8000
第一次 52 ms
第二次 49 ms
第三次 48 ms
# 安装opcache后,再重启Web服务试试
> docker-php-ext-install opcache
# 再次访问,记录耗时
第一次 115 ms
第二次 11 ms
第三次 11 ms
# 退出并自动销毁容器
> exit
- Hyper-V + Bind mounts
# 运行容器
> docker run -it --rm -v E:/laravel8:/var/www/html -p 8000:8000 php:7.4-fpm /bin/bash
# 写性能测试
> dd if=/dev/zero of=test.dat bs=1024 count=100000
100000+0 records in
100000+0 records out
102400000 bytes (102 MB, 98 MiB) copied, 17.4388 s, 5.9 MB/s
# 读性能测试
> dd if=test.dat of=/dev/null bs=1024 count=100000
100000+0 records in
100000+0 records out
102400000 bytes (102 MB, 98 MiB) copied, 0.328262 s, 312 MB/s
# 启动内置Web服务,测试项目访问速度
> php artisan serve --host=0.0.0.0 --port=8000
PHP 7.4.27 Development Server (http://0.0.0.0:8000) started
# 浏览器中访问 http://127.0.0.1:8000
第一次 1.41 s
第二次 656 ms
第三次 616 ms
# 安装opcache后,再重启Web服务试试
> docker-php-ext-install opcache
# 再次访问,记录耗时
第一次 701 ms
第二次 36 ms
第三次 37 ms
# 退出并自动销毁容器
> exit
- Hyper-V + Bind mounts + 一致性选项
绑定挂载的一致性选项:consistent、delegated、cached。
但是这个选项仅仅适用于Docker for Mac。
切换Docker引擎 ‘Use the WSL 2 based engine’,重启计算机
- WSL2 + Bind mounts
# 运行容器
> docker run -it --rm -v E:/laravel8:/var/www/html -p 8000:8000 php:7.4-fpm /bin/bash
# 写性能测试
> dd if=/dev/zero of=test.dat bs=1024 count=100000
100000+0 records in
100000+0 records out
102400000 bytes (102 MB, 98 MiB) copied, 6.20594 s, 16.5 MB/s
# 读性能测试
> dd if=test.dat of=/dev/null bs=1024 count=100000
100000+0 records in
100000+0 records out
102400000 bytes (102 MB, 98 MiB) copied, 5.87995 s, 17.4 MB/s
# 启动内置Web服务,测试项目访问速度
> php artisan serve --host=0.0.0.0 --port=8000
PHP 7.4.27 Development Server (http://0.0.0.0:8000) started
# 浏览器中访问 http://127.0.0.1:8000
第一次 1.73 s
第二次 801 ms
第三次 764 ms
# 安装opcache后,再重启Web服务试试
> docker-php-ext-install opcache
# 再次访问,记录耗时
第一次 1.75 s
第二次 586 ms
第三次 574 ms
# 退出并自动销毁容器
> exit
- WSL2 + Bind mounts + 挂载源目录改为
/mnt/e/
## 进入子系统,以下命令均在子系统执行
# 运行容器
> docker run -it --rm -v /mnt/e/laravel8:/var/www/html -p 8000:8000 php:7.4-fpm /bin/bash
# 写性能测试
> dd if=/dev/zero of=test.dat bs=1024 count=100000
100000+0 records in
100000+0 records out
102400000 bytes (102 MB, 98 MiB) copied, 6.97992 s, 14.7 MB/s
# 读性能测试
> dd if=test.dat of=/dev/null bs=1024 count=100000
100000+0 records in
100000+0 records out
102400000 bytes (102 MB, 98 MiB) copied, 6.67416 s, 15.3 MB/s
# 启动内置Web服务,测试项目访问速度
> php artisan serve --host=0.0.0.0 --port=8000
PHP 7.4.27 Development Server (http://0.0.0.0:8000) started
# 浏览器中访问 http://127.0.0.1:8000
第一次 1.71 s
第二次 761 ms
第三次 760 ms
# 安装opcache后,再重启Web服务试试
> docker-php-ext-install opcache
# 再次访问,记录耗时
第一次 1.72 s
第二次 579 ms
第三次 583 ms
# 退出并自动销毁容器
> exit
- WSL2 + Bind mounts + 挂载源目录改为
//wsl$
WSL2作为后端引擎,Docker Desktop安装了两个专用的内部Linux发行版和,分别用于运行Docker引擎、存储容器和镜像。两者都不能用于一般的开发。
文件资源管理器中输入 \\wsl$ 查看
# 尝试把源码放在 \\wsl$\docker-desktop-data\laravel8
# 无法挂载...
总结
在Windows中,对于源码文件不多的php-fpm项目,选择Hyper-V + Bind mounts 再启用opcache ,基本可以愉快的开发。
但是遇到源码文件特别多的项目,例如magento2,我还是选择远程开发!
选择远程开发
为了方便在Windows编写代码,Linux容器中运行,选择远程开发也是一个很好的方式。如今的IDE对远程开发的支持已经很完美了。
只需在容器中安装ssh服务,映射22端口到主机即可。下面给个例子供大家参考。
Docekerfile
FROM debian:buster
RUN set -xe \
&& apt-get update -y \
&& apt-get install -y --no-install-recommends \
openssh-server
RUN mkdir -p /var/run/sshd \
&& sed -i "s/#PermitRootLogin prohibit-password/PermitRootLogin yes/" /etc/ssh/sshd_config \
&& echo root:123456 | chpasswd
WORKDIR /workspace
CMD ["/usr/sbin/sshd", "-D"]
EXPOSE 22
构建镜像&运行
docker build -t debian-remote-dev-test .
docker run -d -p 2202:22 debian-remote-dev-test
测试SSH连接
ssh root@127.0.0.1 -p 2202
...连接成功...
记录一下我使用PhpStorm远程开发时踩的坑
-
默认同步选项配置未勾选 '本地删除后删除远程文件',然后我在本地重命名或删除了文件,结果远程还保留了之前的文件,导致程序出错了。
-
开发过程中,远程生成的一些辅助文件(代理类),忘记同步到本地,导致本地代码提示不全。
参考
2024 年文章
记录 2024 年的技术文章,涵盖 Golang、Redis、Socket.IO 等技术领域。
Golang Gin + Redis 队列:轻松应对高并发写入数据库
为了应对高并发写入数据库场景下的性能瓶颈,可以采用先写入队列,再批量读取写入MySQL的方案。
近期工作上需要统计用户事件达成率、留存等数据,对此我选择了Golang Gin框架和Redis队列来实现,以降低数据库负载,提高并发写入性能。
方案
-
对于这种非核心业务的数据统计,简单起见直接用Redis队列写入元数据
-
使用Redis Bitmap记录已上报的用户ID,防止重复上报
-
定时刷新队列消息到数据库,根据业务需求选择合适的持久化频率
伪代码
下面以收集用户事件元数据为例,给出部分伪代码(为了简洁忽略错误处理)
主要字段有appcode、date、eventId、userId
- 元数据写入队列
// 检查当日是否已经上报过
// 使用redis位图检查userId在当日是否上报了事件3
// 此外:为了更准确也可再次查询数据库
bitKey := appcode + ":user_reach:" + nowDate + ":e" + strconv.FormatInt(eventId, 10)
exist, _ := Redis.GetBit(ctx, bitKey, userId).Result()
// 请求转为Json字符串,写入队列
// 此外:写入队列之前,可以先对数据进行压缩,以减少队列占用空间
data, err := json.Marshal(request)
Redis.RPush(ctx, QueueKey, data).Result()
// Bitmap记录用户ID已上报
exist, err := Redis.Exists(ctx, bitKey).Result()
Redis.SetBit(ctx, bitKey, userId, 1)
// Bitmap缓存一天
if exists == 0 {
Redis.Expire(ctx, bitKey, 24*time.Hour)
}
- 定时持久化队列消息到MySQL
// 从队列中取出1000条
// 此外:应该定时检查队列长度,超长时取出消费
values, err := Redis.LRange(ctx, QueueKey, 0, 999).Result()
// 处理数据 转换为model结构体
var data []models.UserReachMeta
for _, value := range values {
var meta models.UserReachMeta
json.Unmarshal([]byte(value), &meta)
data = append(data, meta)
}
// 插入数据库
insertCount := DB.Create(&data).RowsAffected
// 从队列中删除已处理的元素
if insertCount > 0 {
Redis.LTrim(ctx, QueueKey, insertCount, -1)
}
源码
Github kaxiluo/gin-data-statistics-api
压测
工具 jmeter
实战指南:使用Socket.IO和Redis构建实时多人在线PK系统
近期开发了一个知识PK系统,主要包括创建房间、加入房间、开始游戏、答题等功能。对此我选用了SocketIO,实现客户端和服务器之间低延迟,双向和基于事件的通信。
技术方案
- Hyperf框架
一个基于Swoole的PHP协程框架
- SocketIO组件
Socket.IO是一款非常流行的应用层实时通讯协议和框架,可以轻松实现应答、分组、广播
组件基于WebSocket实现,房间适配器基于Redis驱动,可以适应多进程乃至分布式场景
- Redis
存储房间和玩家状态
- Postman
一个强大的客户端工具,支持Socket.IO连接,用它来做功能测试十分方便
状态存储
Redis存储
| 数据结构 | Key | Value | 说明 | |
|---|---|---|---|---|
| 房间信息 | Hash | rooms:{room_no} | {"id": 1, "state": "created", "game_number": 1, "sid":"6626326272d7b#1"} | 存储房间ID、状态等基本信息 |
| 玩家集合 | Set | rooms:{room_no}:playerIds | player1,player2 | 存储房间内所有玩家ID |
| 玩家状态 | Hash | rooms:{room_no}:players:{player_id} | {"id": 1, "nickname": "卡夕洛", "avatar": "", "state":"ready", "score":0, "answer_count":0,"time":1713779921} | 存储玩家的ID、昵称、状态、得分等信息 |
| 暂存房间 | String | staging_room:{user_id} | room_no_xx | 房主断线后保留房间,X秒过期,重连后直接使用该房间 |
Context-连接上下文
实例化后的Roomer/Player服务类可以存储在连接上下文
事件
基本流程
房主:创建房间 - 即将开始 - 开始游戏 - 答题 - 对局完成 - 返回房间/解散房间
玩家:加入房间 - 准备/取消准备/离开房间 - 答题 - 对局完成 - 返回房间/离开房间
| 事件 | 说明 |
|---|---|
| create-room | 房主:创建房间并加入,响应玩家列表和房间信息 |
| join-room | 玩家:加入房间,响应玩家列表和房间信息,向房间内广播player-join |
| leave-room | 玩家:离开房间,向房间内广播player-leave |
| player-ready | 玩家:准备,向房间内广播player-ready |
| player-cancel | 玩家:取消准备,向房间内广播player-cancel |
| game-ready | 房主:游戏即将开始,向房间内广播game-ready |
| game-start | 房主:游戏开始,向房间内广播game-start,消息为PK的题目 |
| answer | 答题,向房间内广播分数等信息 |
| return-room | 返回房间,响应为玩家列表和房间信息 |
| room-dissolve | 房主:销毁房间,向房间内广播room-dissolve |
掉线情况
心跳机制来检测玩家是否掉线,如果客户端在一段时间内没有发送心跳包,则认为客户端已掉线
Socket.IO服务器
2.x版本,客户端与服务器建立连接时,服务器向客户端发送pingInterval和pingTimeout参数,用来控制客户端发送ping消息的频率和超时时间
-
玩家掉线后,直接向房间内广播
player-leave -
房主掉线后,在某些情况下(如APP切换到后台被系统杀掉),保留房间一段时间
启动一个定时器,X秒后执行销毁房间和广播事件。如果客户端重新连接并创建房间,优先判断是否有保留房间,有则直接加入房间并清除定时器
对局记录持久化
每场PK结束后需成绩排名,并把对局记录存储到数据库,即结算
- 所有玩家答题完毕
当玩家答题到最后一道时,判断是否所有玩家都已答完,若是则结算
这里可能会有多人同时触发结算,我们使用Redis分布式锁避免并发问题,只有加锁成功的玩家才能进入结算逻辑
- 所有玩家掉线
当玩家掉线时,如果房间状态为PK中,判断该玩家是否为房间最后一人,若是则结算
- 定时任务
定时巡查结算超时的房间
部署
经过测算,每名玩家约占用1kbRedis内存
Docker + 蓝绿部署
蓝绿部署是一种发布新版本软件的策略,它将新版本软件部署在一个与生产环境完全隔离的环境中,称为“蓝环境”。经过测试验证后,再将流量从“绿环境”切换到“蓝环境”,完成新版本软件的发布。
每次发版切换流量后,客户端与旧版本之间建立的长连接不受影响,待等到旧版本上没有连接后再关闭服务即可。