少湖说 | 科技自媒体

互联网,科技,数码,鸿蒙

引子

最近接触了一个抽奖的项目,由于用户量比较大,而且第三方提供的认证接口并发量有限,为了保证服务的高可用性,所以对高并限制发有一定的要求。经过一系列研究和讨论,做出了以下一些优化方案。

需求分析

  • 根据用户量和日活情况,估算出并发值在100左右,所以该项目的并发量就当在100上以,初期目标定为600-800

  • 特定页面的并发量不超过300,为了保证不对第三方服务造成访问压力,特将并发控制在150以内

  • 由于奖品数量有限,故得奖时,需要进行并发写入控制,防止奖品超发

实施步骤

前端方面

减少客户端访问次数。

  • 使用CDN对网站的静态资源进行优化,可以答复静态请求次数。

  • 抽奖结果一次性生成。

每个用户有有一次抽奖机会,但只能有一次中奖,其他两次随机弹出推荐产品。所以有一次抽奖中,只需要访问一次服务器的抽奖接口。

  • 使用本地缓存。

将抽奖次数,是否中奖等信息记录在本地,避免超次抽奖和多余的服务器请求。例如,一旦该用户中过将,就不需要再访问服务器抽奖接口。

服务端优化

使用缓存

  • 使用服务端缓存。

将页面和相关数据查询进行缓存,减少数据库访问次数。

部分业务逻辑异步处理

对于不需要实时处理的业务逻辑,压入队列,实现异步处理。例如优惠券的发放。

Nginx 优化

  • 提高 Nginx 处理性能

使用 worker_processesworker_cpu_affinityworker_rlimit_nofileworker_connectionsopen_file_cache等命令,提高nginx的处理性能。

  • Nginx 并发控制。

先看实例,如下:

1
2
3
4
5
6
limit_req_zone $binary_remote_addr zone=req_ip:10m rate=40r/s; // #每个IP平均处理的请求频率为每秒40次
limit_conn_zone $binary_remote_addr zone=conn_ip:10m;
limit_conn_zone $server_name zone=conn_server:10m;
limit_conn conn_ip 5; // #限制某个IP来源的连接并发数,此处为5个
limit_conn conn_server 600; //#限制某个虚拟服务器的总连接数,此处为600个
limit_req zone=req_ip burst=5; //小为5的缓冲区, 当有大量请求过来时,超过了访问频次限制的请求可以先放到这个缓冲区内

在这里,限制了每个IP的请求频率,限制了同一IP的并发连接数,限制了服务器的总连接数

  • 使用 Docker 实现负载均衡

配置 nginx, 使用负载均衡

1
2
3
4
5
6
7
upstream icontact_pool {
server web:9000 weight=5 max_fails=3 fail_timeout=10s;
server web2:9000 weight=5 max_fails=3 fail_timeout=10s;
server web3:9000 weight=5 max_fails=3 fail_timeout=10s;
...
}

如上,通过 Docker 启动多个处理请求的服务容器,在 nginx 中配置每个服务的地址,权重等信息,扩大请求的处理能力

  • 其他服务器环境优化

例如,增加服务器配置(CPU,内存,带宽),如果是PHP, 开启 opcache, 并使用较新版本(php7+), 各种依赖尽量使用最新版本。

1
2
3
4
zend_extension=opcache.so
opcache.enable=1
opcache.enable_cli=1

参考资料

问题

按照以往的做法,你会使用hexo来生成博客的静态文件,并通过git提交到github上,以此来写作和发布文章。

然而,经过一段时间,你会发现,这种写作方式存在几个问题:

  • 文章源和相关编译、部署程序都存储在本地,如果在其他设备上写作,没有同步措施。这样书写多有不便。
  • 文章源没有备份,也没有充分使用Git来管理。
  • 每次书写完文章,需要手动编译和部署。

鉴于以上几个问题,结合Travis-CI的使用经验,我决定对往常的写作方式进行改进:

步骤

新建分支

除了原来博客中的master分支, 再新建两个分支:docs分支用于存放文章源:hexo分支用于存放一些配置文件,我们将在该分支上进行持续部署。

docs 分支

该分支下存放文章源,各篇均以markdow格式书写。除此之外,有 .travis.yml 配置文件和一个触发部署的脚本 deploy.sh

每当有文章提交上来,就会自动触发一次构建,其内容是通过调用Travis-CI的API,自动执行一次 hexo 分支上的自动构建。

hexo 分支

该分支存放一些hexo的配置文件,当构建被触发时,会自动拉取docs中的文章内容,安装一个material皮肤,并进行hexo的自动网站编译。
最后编译完的静态Html,强制推送到master分支,从而博客自动更新。

简介

Code Climate 是一个代码测试工具, 它可以帮助你进行代码冗余检测、质量评估,同时支持多种语言,如PHP, Ruby, JavaScript, CSS, Golang, Python 等。

使用

配置GitLab Runner

1
2
3
4
5
6
7
8
9
10
11
[[runners]]
....
executor = "docker"
[runners.docker]
tls_verify = false
image = "docker:latest"
privileged = true
disable_cache = false
cache_dir = "cache"
volumes = ["/cache", "/var/run/docker.sock:/var/run/docker.sock", "/tmp/builds:/builds"]
shm_size = 0

注意, 需要增加一个 /tmp/builds:/builds , 这里用于映射放代码。否则根据官方文档中的描述,无法正常实现

为了能使用宿主机的docker 缓存, 加快构建速度, 这里使用 sock 绑定的方式使用docker, 不使用 docker in docker

配置 .gitlab-ci.yml 文件

1
2
3
4
5
6
7
codeclimate:
image: docker:latest
script:
- docker pull codeclimate/codeclimate
- VOLUME_PATH=/tmp/builds"$(echo $PWD | sed 's|^/[^/]*||')"
- docker run -v /tmp/cc:/tmp/cc -v $VOLUME_PATH:/code -v /var/run/docker.sock:/var/run/docker.sock codeclimate/codeclimate validate-config
- docker run --env CODECLIMATE_CODE="$VOLUME_PATH" -v /tmp/cc:/tmp/cc -v $VOLUME_PATH:/code -v /var/run/docker.sock:/var/run/docker.sock codeclimate/codeclimate analyze -f text

配置 .codeclimate.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
engines:
duplication:
enabled: true
config:
languages:
- javascript
- php
csslint:
enabled: true
eslint:
enabled: true
fixme:
enabled: true
phpmd:
enabled: true
ratings:
paths:
- "**.js"
- "**.css"
- "**.php"
exclude_paths:
- tests/
- vendor/

相关配置请参考官方文档

参考资料

简介

rsync命令是一个远程数据同步工具

主要参数

  • -r 递归目录

  • -t 保留修改时间

  • -v 详细日志

  • -h 输出数字以人类可读的格式

  • -z 在传输过程中压缩文件数据

  • -e 指定要使用的远程shell, 注意该过程需要注入SSH

配置参考

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

before_script:
- 'which ssh-agent || ( apk update && apk add openssh-client)'
- apk add rsync
- eval $(ssh-agent -s)
- echo "$SSH_PRIVATE_KEY" > ~/deploy.key
- chmod 0600 ~/deploy.key
- ssh-add ~/deploy.key
- mkdir -p ~/.ssh
- '[[ -f /.dockerenv ]] && echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config'
- export APP_ENV=testing


testing-server:
stage: deploy
image: alpine
variables:
DEPLOY_SERVER: "server-host"
script:
- cd deploy
- rsync -rtvhze ssh . root@$DEPLOY_SERVER:/data/$CI_PROJECT_NAME --stats

注意

远程服务器需要安装rsync, 否则会出现 bash: rsync: command not found 错误

参考资料

简介

LFTP是一款FTP客户端软件, 支持 FTP 、 FTPS 、 HTTP 、 HTTPS 、 SFTP 、 FXP 等多种文件传输协议。

本文介绍如何使用 LFTP 将文件同步到远程FTP服务器上, 从而实现自动部署

mirror 命令及主要参数

  • -R 反向传输, 因为是上传(put)到远程服务器, 所以使用该参数 (默认是从远程服务器下载)

  • -L 下载符号链接作为文件, 主要处理文件软链接的问题

  • -v 详细输出日志

  • -n 只传输新文件 (相同的旧文件不会传输, 大大提升了传输效率)

  • –transfer-all 传输所有文件, 不论新旧

  • –parallel 同时传输的文件数

  • –file 本地文件

  • –target-directory 目标目录

配置参考

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
deploy:
stage: deploy
dependencies:
- installing-dependencies
script:
- apk add lftp
# 只上传新文件
- lftp -c "set ftp:ssl-allow no; open -u $FTP_USERNAME,$FTP_PASSWORD $FTP_HOST; cd /wwwroot; mirror -RLnv ./ /wwwroot --ignore-time --parallel=50 --exclude-glob .git* --exclude .git/"
# 指定目录覆盖上传 (强制更新)
- lftp -c "set ftp:ssl-allow no; open -u $FTP_USERNAME,$FTP_PASSWORD $FTP_HOST;mirror -RLv ./vendor/composer /wwwroot/vendor/composer --ignore-time --transfer-all --parallel=50 --exclude-glob .git* --exclude .git/"
# 单独上传autoload文件(强制更新)
- lftp -c "set ftp:ssl-allow no; open -u $FTP_USERNAME,$FTP_PASSWORD $FTP_HOST;mirror -Rv --file=vendor/autoload.php --target-directory=/wwwroot/vendor/ --transfer-all"
only:
- master

参考资料

简介

PHPMD是与PMD类似的静态代码分析工具, 通过分析可以找出潜在的Bug或设计问题, 从而进一步提高代码质量

使用

  • 首先通过composer安装phpmd库
1
2
composer require phpmd/phpmd --dev --prefer-dist 

  • 运行phpmd命令
1
2
vendor/bin/phpmd ./ text phpmd.xml --suffixes php

phpmd.xml配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<?xml version="1.0"?>
<ruleset name="PHPMD rule set for Yii 2" xmlns="http://pmd.sf.net/ruleset/1.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://pmd.sf.net/ruleset/1.0.0 http://pmd.sf.net/ruleset_xml_schema.xsd"
xsi:noNamespaceSchemaLocation="http://pmd.sf.net/ruleset_xml_schema.xsd">
<description>Custom PHPMD settings for naming, cleancode and controversial rulesets</description>

<rule ref="rulesets/naming.xml/ConstructorWithNameAsEnclosingClass" />
<rule ref="rulesets/naming.xml/ConstantNamingConventions" />
<!-- Long variable names can help with better understanding so we increase the limit a bit -->
<rule ref="rulesets/naming.xml/LongVariable">
<properties>
<property name="maximum" value="25" />
</properties>
</rule>
<!-- method names like up(), gc(), ... are okay. -->
<rule ref="rulesets/naming.xml/ShortMethodName">
<properties>
<property name="minimum" value="2" />
</properties>
</rule>

<rule ref="rulesets/cleancode.xml">
<!-- else is not always bad. Disabling this as there is no way to differentiate between early return and normal else cases. -->
<exclude name="ElseExpression" />
<!-- Static access on Yii::$app is normal in Yii -->
<exclude name="StaticAccess" />
</rule>

<rule ref="rulesets/controversial.xml/Superglobals" />
<rule ref="rulesets/controversial.xml/CamelCaseClassName" />
<rule ref="rulesets/controversial.xml/CamelCaseMethodName" />
<rule ref="rulesets/controversial.xml/CamelCaseParameterName" />
<rule ref="rulesets/controversial.xml/CamelCaseVariableName" />
<!-- allow private properties to start with $_ -->
<rule ref="rulesets/controversial.xml/CamelCasePropertyName">
<properties>
<property name="allow-underscore" value="true" />
</properties>
</rule>
</ruleset>

GitLab-CI 集成

在.gitlab-ci.yml中添加一个任务, 用于执行静态分析, 一个典型的例子:

1
2
3
4
5
6
phpmd:
stage: testing
dependencies:
- installing-dependencies
script:
- vendor/bin/phpmd api,backend,common,frontend,console text phpmd.xml --exclude console/migrations/ --suffixes php

参考资料

Github

简介

Fixtures 是测试中非常重要的一部分。主要目的是建立一个固定/已知的环境状态以确保 测试可重复并且按照预期方式运行。

简答说就是Fixtures提供一种预填充数据的方式,即在测试前需要准备好哪些数据,以便测试可以正常展开,不受其他测试的影响。

一个 Fixture 可能依赖于其他的 Fixtures ,所定义的依赖会自动加载。

该方法相比于dump.sql的填充方法更加灵活, 且不会出去填充的冲突问题.

配置

定义一个Fixtures

通过继成yii\test\ActiveFixture, 并声明 modelClass 来定义一个Fixtures , depends为要依赖的Fixtures, 可选。

Fixtures通常放置于tests目录中的fixtures目录下.

1
2
3
4
5
6
7
8
9
10
11
<?php
namespace tests\fixtures;

use yii\test\ActiveFixture;

class UserFixture extends ActiveFixture
{
public $modelClass = 'app\models\User';
public $depends = ['app\tests\fixtures\UserFixture'];
}

设置填充数据

@tests/fixtures/data目录中,每个Fixtures添加一个数据文档

在位置 @tests/fixtures/data/user.php 中, 设置以下数据, 为要被插入用户表中的数据文件, user1user2为别名, 方便调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
return [
'user1' => [
'username' => 'lmayert',
'email' => 'strosin.vernice@jerde.com',
'auth_key' => 'K3nF70it7tzNsHddEiq0BZ0i-OU8S3xV',
'password' => '$2y$13$WSyE5hHsG1rWN2jV8LRHzubilrCLI5Ev/iK0r3jRuwQEs2ldRu.a2',
],
'user2' => [
'username' => 'napoleon69',
'email' => 'aileen.barton@heaneyschumm.com',
'auth_key' => 'dZlXsVnIDgIzFgX4EduAqkEPuphhOh9q',
'password' => '$2y$13$kkgpvJ8lnjKo8RuoR30ay.RjDf15bMcHIF7Vz1zz/6viYG5xJExU6',
],
];

使用

在测试用例中, 通过定义_fixtures方法, 声明需要使用的Fixtures及填充数据文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

/**
* @var \UnitTester
*/
protected $tester;

public function _fixtures()
{
return [
'users' => [
'class' => UserFixture::className(),
'dataFile' => '@tests/fixtures/data/user.php'
],
'profiles' => [
'class' => UserProfileFixture::className(),
'dataFile' => '@tests/fixtures/data/user_profile.php'
],
];
}

通过如下方法, 可以获取插入的记录, 返回值为该Fixture类中对应的modelClass的一个实例

1
2
$user = $this->tester->grabFixture('users', 'default');

参考资料

Fixtures
Fixtures

概述

如果多个项目中存在使用相同类库、模块的情况,此时可以考虑将类库或者模块单独抽取出来,形成独立类库,通过composer
来进行依赖管理,这样可以更方便维护,大大提升开发效率。

优势

  • 可以对特定模块进行统一维护和升级
  • 特定的类库可由专人进行维护,保证稳定性和可靠性
  • 避免了重复开发的情况

步骤

本地开发

为了方便调试,可先在本地现有项目中开发类库,等到开发完成后,再将相关代码单独抽取出来。

  • 首先在项目中创建一个存放类库的目录,如packages/zacksleo/my-libs,

其中packages是类库总目录, zacksleo是用户名,相当于命名空间的第一级,my-libs是类库存放目录。

  • 在目录中创建composer.json 文件,并添加形如以下的内容:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"name": "zacksleo/my-libs",
"description": "my libs",
"type": "library",
"license": "MIT",
"authors": [
{
"name": "zacksleo",
"email": "zacksleo@gmail.com"
}
],
"minimum-stability": "stable",
"autoload": {
"psr-4": {
"zacksleo\\my\\libs\\": "src"
}
}
}

其中,name是类库名称,descrption是详细说明,type是类别,license是使用的协议,authers是作者信息,

minimum-stability 用来声明最小依赖,通常有devstable可选,autoload中的psr-4声明了

命名空间和对应的目录,注意命名空间就当使用双反斜杠,目录使用相对路径,此外声明了目录为`src``目录

  • src目录中添加相关代码,其中的类使用命名空间zacksleo\\my\\libs

  • 在项目的composer.json中,通过path方式引入本地类库,如可在repositories中添加如下信息:

1
2
3
4
5
6
7
"repositories": {
"my-libs": {
"type": "path",
"url": "packages/zacksleo/my-libs"
}
}

其中my-libs是别名,可任意填写,type设置成path, url为类库所在的相对路径(与composer.json文件相对)

  • 通过composer require命令或者在composer.json中的require部分添加声音,来实现依赖加载,如

composer require zacksleo/my-libs

在Github上创建库并上传代码

当在本地开发完成后,可将类库独立抽取出来(此处的my-libs目录下的内容),并提交到Github上新建的仓库中

配置packagist并发布

  1. 先在packagist.org中注册好账号,以便发布包。
  2. 在Github的仓库中,点击settings,找到 Intergrations & services, 点击Add servies, 选择Packagist,

填写在packagist.org注册的用户名和Token(在Profile中找到Your API Token)

点击确定添加,这样,每次Github的变动,都会自动更新到packagist上,免去了手动更新的麻烦

本地依赖改成线上版本, 并清除开发代码

类库一经发布到packagist上后,就可将本地项目composer.json添加的repositories移除,重新运行composer install

来安装packagist上的版本,同时packages 目录亦可删除。

版本问题说明

composer使用语义化的版本进行依赖管理,因此类库在更新和发布时,所标记的版本号,也就当遵循语义化的版本规范

基主要有以下几个内容:

版本格式:主版本号.次版本号.修订号,版本号递增规则如下:

  1. 主版本号:当你做了不兼容的 API 修改,
  2. 次版本号:当你做了向下兼容的功能性新增,
  3. 修订号:当你做了向下兼容的问题修正。
  4. 先行版本号及版本编译信息可以加到“主版本号.次版本号.修订号”的后面,作为延伸。

参考资料

GitLab-Ci实现自动化测试

持续集成的目的,就是让产品可以快速迭代,同时还能保持高质量。它的核心措施是,代码集成到主干之前,必须通过自动化测试。只要有一个测试用例失败,就不能集成。

使用自动化测试, 可以提高软件的质量和可靠性, 今早发现其中的缺陷和问题, 以便即时改正.

配置环境

首先需要一个满足运行自动化测试的Docker镜像, 以便后面运行测试代码, 例如:

1
2
image: zacksleo/docker-composer:develop

配置服务

某些测试需要使用额外的服务, 如数据库、缓存服务器等等, 并通过variables配置服务中的一些变量

1
2
3
4
5
6
7
8
services:
- mysql:5.6
- redis:latest
variables:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: web
MYSQL_USER: web
MYSQL_PASSWORD: web

声明依赖工件

一般在测试前要进行准备过程, 如安装依赖库或者编译等, 可将上述过程生成的的文件, 通过依赖声明, 传递过来, 这样可以比避免重复执行相关过程.

1
2
3
4
5
6
7
8
9
10
11
installing-dependencies:
stage: prepare
script:
- composer install --prefer-dist --optimize-autoloader -n --no-interaction -v --no-suggest
- composer dump-autoload --optimize
artifacts:
name: "vendor"
untracked: true
expire_in: 60 mins
paths:
- $CI_PROJECT_DIR/vendor
1
2
3
dependencies:
- installing-dependencies

配置测试脚本

假定你已经在本地编写好了测试代码, 并且可以本地运行, 那么就可以通过调整和适配, 让测试可以在GitLab-CI中自动化执行, 在下面的例子中,

测试代码位于tests目录,并且.env中配置了一些环境变量, 该文件的作用是为了让不同环境使用不同的一组变量, 如数据库、接口地址、账号等等,
这样做的目录可以尽量少的变更代码,保持核心代码的稳定性和适应能力, 通过php -S 启动了一个本地接口服务, 最后调用api测试, 对所有接口
进行测试

在下面的例子中, 还声明了coverage, 这个用来说明代码测试覆盖率的取得方法, 因为在测试中会将覆盖率输出(--coverage --no-colors),
GitLab-CI 通过正则匹配输出内容, 读取到覆盖率, 从而显示在项目徽标处

1
2
3
4
5
6
7
8
9
dependencies:
- installing-dependencies
script:
- cp tests/.env .env
- ./yii migrate/up --interactive=0
- php -S localhost:80 --docroot api/tests &>/dev/null&
- ./vendor/bin/codecept run api -c tests --coverage --no-colors

coverage: '/^\s*Lines:\s*\d+.\d+\%/'

测试失败如何处理

当测试失败后, 除了查看Pipline中的任务输出, 我们还应当详细查看测试中的相关日志, 下面这里, 将需要查看的文件生成工件, 在GitLab中下载,
然后可以在本地详细查看, when说明了仅在测试失败时, 才生成工件

1
2
3
4
5
6
7
8
artifacts:
name: "debug"
when: on_failure
untracked: true
expire_in: 60 mins
paths:
- $CI_PROJECT_DIR/api/runtime
- $CI_PROJECT_DIR/tests/_output

完整的例子

下面是一个完整的API自动化测试的盒子

api-test:
    stage: testing
    services:
        - mysql:5.6
        - redis:latest
    variables:
        MYSQL_ROOT_PASSWORD: root
        MYSQL_DATABASE: web
        MYSQL_USER: web
        MYSQL_PASSWORD: web
    dependencies:
        - installing-dependencies
    script:
        - cp tests/.env .env
        - ./yii migrate/up --interactive=0
        - php -S localhost:80 --docroot api/tests &>/dev/null&
        - ./vendor/bin/codecept run api -c tests
    artifacts:
        name: "debug"
        when: on_failure
        untracked: true
        expire_in: 60 mins
        paths:
            - $CI_PROJECT_DIR/api/runtime
            - $CI_PROJECT_DIR/tests/_output
    only:
        - develop
        - master

关于持续集成完整的项目, 请查看 zacksleo/yii2-app-advanced 项目

使用Docker镜像

0%