Lumen + Swoole 探坑小记

注意:本文以 Lumen 为对象进行写作,部分内容适用于 Laravel 但考虑到 Lumen 和 Laravel 在部分实现上有所差异,所以不保证本文提到的内容都也适用于 Laravel。本文写作时间点 Lumen 的版本是 v5.7.4 ,laravel-swoole 的版本是 v2.5.0。
考虑到随着未来版本的更新,部分特性会发生变化,所述内容仅供参考。

Swoole 是一个已经被广泛验证过的生产级别的PHP高性能异步网络框架,其近期逐渐完善的协程特性,更是极大提高了编码的体验。

目前查到这方面的文章大多数 Swoole 非常好 , Swoole 的运行机制能够大幅改善 Laravel、Lumen 的性能,让我们一起装一下 Swoole ,引入一个库,Webbench 对比看一下吧。

但实际上手完成应用,而不仅仅是一个 echo server ,就不是这么美好的事情,将 Swoole 集成到先有的应用系统中,也不仅仅是单靠引入一个库就能完成的。因此作为探坑学习,相对来说轻量级,并不包含过多组件的 Lumen 就是一个不错的选择。

要玩 Swoole 首先还是需要为 PHP 安装 Swoole 扩展,Linux环境下,可以直接使用PECL进行安装。当然,考虑到环境的复杂性,最省心的做法还是使用 Docker。

目前为 Lumen 增加 Swoole 支持的库有 laravel-s 和 laravel-swoole 两个,由于laravel-s 方案默认关闭并且不推荐开启协程,因此这次探坑选择了 laravel-swoole 方案。

在开始探坑前,首先还是需要了解一下 Swoole 的运行机制,以及 laravel-swoole 这个库到底做了什么。这边借用 laravel-swoole 的 Wiki 上的图。

Swoole 服务 通过自行PHP脚本启动之后,Swoole 会创建主要的反射器和管理器,进行管理调度,其中 主反射器 的工作模式和 Nginx 类似。而管理器则用于调度 Woker 和 Task Worker。而实际请求处理逻辑则是在 Worker 里面完成的。

对于 Lumen 来说,它的 Application 会在 Worker 启动时被创建,并且常驻内存,而不会每次接收请求都重新创建,这会有很好的性能,但问题也显而易见,原先我们并不管新的 全局属性、类的静态属性、单例 都会成为问题。

对于 Lumen 来说,其核心的特性,则在于 Service Container,Container 中的单例不会重新创建,每次请求是复用之前的,那之前的请求过程势必会引入很多污染,这些污染如果不清理的话,被带入到之后的请求,就可能引起后面请求的结果和预期不一致。但作为最大单例的 Application 如果清理掉,重新创建的话,那就失去了其加载常驻内存的意义了,因此 laravel-swoole 库采用的方式是引入了 沙盒 Sandbox。

Sandbox 机制 与 静态访问安全

在前面提到 常驻内存 需要格外注意的有 类的静态属性 ,那是不是我们自己写的类注意了就好呢,并不全是,因为在 Lumen 实现本身,也再使用静态属性。最典型的地方便是 Container 和 Facade 。Lumen 借助 Container 类的 静态属性 $instance 实现单例。而 Facade 则使用 静态属性 $app 保存 Application 实例,使用 $resolvedInstance 保存已经通过 Container 解析好的 Facade 实例。

那么 laravel-swoole 的 Sandbox 机制是怎么运作的呢,首先 Swoole 实际请求执行都是在 Worker 中执行的,laravel-swoole 在 Worker 被创建时,创建了 Sandbox 并且对 Lumen 的 Application 进行了初始化,同时预解析了一些常用的,同时不会因为不同请求处理之间会产生污染的 单例,这个预解析部分在其配置文件中是可以配置的。

接下来对于每一次请求来说,Sandbox 都会克隆一个已经创建好的 Application 的副本,并且通过预设的 重置器(Resetter) 对确保克隆的 Application 内已经创建的单例如果有已经绑定 Application 的,重新都绑定到当前克隆后的 Application 上,接着由这个克隆后的副本来处理本次请求的内容。这样看着很完美,但还有什么需要注意的呢,那便是前面说的 Lumen 的静态属性。Container 和 Facede。静态属性在同一个 Worker 中是公共的,不会因为不同请求有 独立的 Application ,静态属性就能被隔离。这个问题无解的话,laravel-swoole 目前的做法是在每一次创建时把当前的 Application 设置到 Container 的 $instance 上,同时清空 Facade 的 $resolvedInstance 。这对于同一时间一个Worker只处理一个请求来说是没问题的,但是 Swoole 的协程特性,使得在协程切换时,Worker 是可以接受并处理下一个分配的请求的。这就使得但凡涉及到 访问 Container 单例 和 Facade 进行服务解析都需要被格外注意。而最典型的就是 服务扩展,以及最为敏感的每次服务的鉴权问题。

对于 Lumen来说,Lumen 提供 app 帮助函数进行的服务解析依赖于 Container 单例,而不管使用哪个 Facade 都依赖于 Facade 的静态属性,因此这两种使用方式是需要被注意的。

比较多见的一种情况是在 Service Provider 中对 某个 Application 的 AuthManager 扩展了 guard 或者 guard driver。但如果使用时,通过Container得到的 AuthManager是新创建的,没有被 Service Provider扩展,则可能会遇到 guard 或者 drive 找不到的错误。

Auth guard [{$name}] is not defined.
Auth driver [{$config[‘driver’]}] for guard [{$name}] is not defined.

laravel-swoole 的文档的 Debug Guideline 会建议,如果遇到授权问题时,需要检查每一次请求的 AuthManager 是不是会被重新解析。但是不是重新解析的关键并不是解析本身,而是何时使用何种方式触发解析,怎么样才是安全的。

前面提到 Sandbox 对于 每一个请求 都会创建一个对应的 Application 的副本,Lumen 的 Application 也充当 Service Container 的功能,因此对于每一个请求都需要单独解析的服务类来说,使用 请求本身的 Application 副本 而不是 公共的 Container 单例、或者 Facade ,是一个相对来说安全的做法。

对于 Service Provider 来说,如果需要获取请求本身的 Application 副本,做法是将 Service Provider 添加到 laravel-swoole 配置文件中的 providers 中,从而laravel-swoole在每次重置 Application 的时候,会重新把新的Application关联到 Service Provider 中,并重新执行 Service Provider 。而在 Service Provider 中,则需要使用 $this->app 来访问被重新关联上的当前请求的  Application 副本。

对于其他类来说,如果需要获取请求本身的 Application 副本,那么最常见的做法就是在构造函数接受参数中接受 Application,并且使用 Application 副本的 Service Container 进行解析,通过依赖注入的方式进行获取。

当然,对于全局公共的,并不涉及到限定于本次请求的类来说,保持原来的方式,不管是用 Sandbox 一开始初始化的 Application 中获取,还是从 某一次请求的 Application 中获取,都是没有问题的。

MySQL 中 ONLY_FULL_GROUP_BY 模式

ONLY_FULL_GROUP_BY 对 GROUP BY 查询的限制

在 SQL 中 利用 GROUP BY 聚合 可以起到将字段相同的记录合并的目的。但也常常被滥用,以及使用不当出现结果与预期不一致的情况。

在 sql_mode 中提供了 ONLY_FULL_GROUP_BY 用以限制在 select 字段中 使用 group by 之外的字段 但没有使用聚合函数的情况。

例如:

select type, price from products group by type 

结果根据 type 聚合,但 price 未使用聚合函数,在未开启 ONLY_FULL_GROUP_BY 的情况下 mysql 会依照聚合顺序 返回默认的结果,而开启之后,则会返回错误:

ERROR 1055 (42000): Expression #2 of SELECT list is not in GROUP BY clause and contains nonaggregated column ‘db.tbs.price’ which is not functionally dependent on columns in GROUP BY clause; this is incompatible with sql_mode=only_full_group_by

当前所使用的 sql_mode 可以使用 show variables 查看

show variables like 'sql_mode';

MySQL 5.7 新增 ANY_VALUE()

从 MySQL 5.7 版本开始 默认将 ONLY_FULL_GROUP_BY 设置为启用状态。因此 如果依旧按照原先的方式进行查询 将会出现错误,在开启 ONLY_FULL_GROUP_BY 时 如果对非 GROUP BY 字段需要获取同原先一样的结果,5.7 版本开始则提供了一个 ANY_VALUE() 函数:

select type, ANY_VALUE(price) from products group by type 

Laravel 的 strictMode

对于 Laravel 框架 在 config/database.php 文件启用严格模式(strict 为 true )时 ONLY_FULL_GROUP_BY 设为启用状态

strict 为 true
strictMode 下启用 ONLY_FULL_GROUP_BY

如果使用的 MySQL 版本高于 5.7 则 优先考虑使用 ANY_VALUE() 函数
如果还在使用低版本 MySQL 又不方便升级,可以考虑将 strict 设为 false 关闭 strictMode

基于Docker 项目PHP环境运行 Composer 并使用国内镜像源

使用Docker可以快速在不同机器上运行构建好一致的运行环境,越来越多的被用在项目生产中。而 Composer 基本是PHP开发必不可少的包管理工具。

但在实际PHP开发部署过程中,Composer对于环境与依赖的判断源自于运行Composer的PHP环境,不同的运行方式,对于Composer的执行情况产生运行。

因为Composer限制root权限运行,因此Docker容器并不能直接shell运行Composer,如果直接在本机安装 Composer ,那么则依赖于机器本身的 PHP 环境,而使用Composer官方的镜像,则基于构建源的PHP版本环境。而在项目开发中,为了保证可靠,还是应用使用与项目一致的 PHP环境来运行 Composer。

首先 为项目 PHP 的 Dockerfile 增加 Composer 可以直接使用 Docker 的 multi-stage 功能直接从官方源复制

FROM php:7.1-fpm # 或者指定你自己build好的镜像
#
# .... 自身项目PHP环境配置 ... 建议直接基于已经 build 好的镜像
#

# 增加 TINI (这里直接使用二进制 也可以使用镜像对应的包管理安装)
ENV TINI_VERSION v0.17.0
ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /sbin/tini
RUN chmod +x /sbin/tini

# COPY composer
ENV COMPOSER_ALLOW_SUPERUSER 1
COPY --from=composer:1.6 /usr/bin/composer /usr/bin/composer # 在镜像中增加 composer
COPY --from=composer:1.6 /docker-entrypoint.sh /composer-entrypoint.sh
WORKDIR /app
ENTRYPOINT ["/composer-entrypoint.sh"]
CMD ["composer"]

build 镜像 并标记 tag

docker build -t my-composer ./

在本地创建持续存储 composer 全局数据目录

mkdir ~/.composer

配置本地 .bash_composer 环境增加 composer 命令运行

composer () {
tty=
tty -s && tty=--tty
docker run \
$tty \
--interactive \
--rm \
--user $(id -u):$(id -g) \
--volume $(pwd):/app \
--volume $HOME/.composer:/tmp/composer \
-e "COMPOSER_HOME=/tmp/composer" \
my-composer "$@"
}

在 .bash_rc 或者引用 .bash_composer 文件,使用 source 命令引入

. "~/.bash_composer"

开始使用,并且配置中国镜像

composer config -g repo.packagist composer https://packagist.laravel-china.org

Lumen 5.4 修改 PDO Fetch Mode

项目需要采用了 Lumen 来开发 RESTful API 但又有需要修改 PDO 的 Fetch Mode 的需求

由于 Lumen 文档并没有关于此的特别说明 便参照 Laravel 过往的做法试图配置 config/database.php 并不起作用

在 翻阅 Laravel 文档中关于 升级变更的说明时才提到 在几周前发布的 5.4 版本 中不再支持 在配置文件中自定义 Fetch Mode (其实本身 Laravel 也一直不推荐修改)

如果需要这么做的话 目前采用的方案是 监听 Illuminate\Database\Events\StatementPrepared 事件 进行配置

对应到 Lumen 中也是一样的 可以在 EventServiceProvider 的 boot 方法中 设置

use Illuminate\Database\Events\StatementPrepared;
public function boot()
{
// 修改 PDO Fetch Mode
Event::listen(StatementPrepared::class, function($event) {
$event->statement->setFetchMode( ... );
});

// 其他自定义监听事件
}

当然 如果之前没有启用 EventServiceProvider 的话 还需要在 bootstrap/app.php 启用

// 去除前面的注释
$app->register(App\Providers\EventServiceProvider::class);

先导者卡牌资料站

恩 又开了一个新坑 这次是和 THE一灭寂 合作完成的先导者卡牌资料站

#卡片战斗先导者# 在线卡牌资料非官方中文综合站·《先导者卡牌资料站》今日开始公开测试!
地址:http://vgcard.yimieji.com
设计&资料:THE一灭寂
开发:@smdcn
PC与手机浏览器两者均可使用!具备卡片搜索、卡片详情查看、规则释疑(此项暂未填写完毕)等功能!
目前为开放测试期间,希望大家积极向我们反馈使用后的意见或建议。我们在完善暂未完成的项目的同时,也会根据大家的反馈尽可能进行优化!谢谢大家的支持!

继续阅读先导者卡牌资料站

iOS 创意打砖块游戏「Outbreak: Legacy」

我们小时候都玩过弹珠打方块的游戏,在新的时代,打方块游戏又能有怎么样的变化呢,于是从环形轨道出发,便开始游戏制作的探索。于是经过了和小伙伴一块儿一年多的努力,我们的游戏「Outbreak: Legacy」(中文:OO弹球 – 新式环形打砖块)目前终于在苹果App Store发布啦,同时获得“本周新游”推荐。

「Outbreak: Legacy」是一款以新奇砖块嘉年华为主题的环形弹球游戏,通过融入超多的独特创新元素,探索在环形轨道上,操控挡板,反弹共多种技能各异的小球。游戏包含了5个不同主题、数十个精心设计的关卡、并设置了BOSS关与无尽挑战。

更多丰富内容欢迎大家来体验试玩,游戏 App Store下载地址:https://appsto.re/cn/jaCxcb.i

appstore

继续阅读iOS 创意打砖块游戏「Outbreak: Legacy」

在Docker中使用代理

我们常常会因为下面这些原因需要使用代理:

  • 本机本身访问网络需要经过代理
  • 本地访问Docker Hub过慢需要加速

而Docker本身又是由服务端与客户端构成

作用 使用代理
Docker客户端 用于连接服务端 向服务端发送指令及本地数据 设置代理则会通过代理与服务端进行连接
当我们docker服务端和客户端并不在同一设备上时 docker 使用 https 进行通信,而docker客户端是可能会受到http_proxy、 https_proxy环境变量影响的。可能的原因因为Docker客户端版本不同或者其他原因造成有时会影响有时不会影响

如果在客户端的命令行设置了 http_proxy、 https_proxy 之后,客户端会通过代理链接服务端,此时,如果代理不可到达服务器(比如OSX中docker服务端在本地的虚拟机中,使用Host-only方式与本机链接,如果使用的代理并非本机,则代理无法到达本地虚拟机种的docker服务端),则会导致docker命令无法连上服务端。
在这种情况下,在需要使用docker命令的命令行环境,需要 unset http_proxy 、https_proxy 确保客户端直接与服务端进行连接。

Docker服务端 Docker的执行进程 对外提供接口
执行收到的命令(包含实际访问Docker Registry、Docker Hub)
通过代理进行对应的网络请求
在Bash运行环境中设置的 http_proxy、 https_proxy、 no_proxy docker的服务端并不能识别 参考 Github Issue #402 Using Docker behind a firewall。

 

 

Docker服务端设置代理(加速)

1. 给Docker配置代理

在启动Docker服务端的命令行前直接加入: HTTP_PROXY=server:port

例如直接启动时使用:
sudo HTTP_PROXY=server:port docker -d &

或者在docker的服务启动文件(如:/usr/local/etc/init.d/docker)找到
/usr/local/bin/docker -d,在前面加入 HTTP_PROXY=server:port

2. 使用Daocloud加速器

如果Docker服务器并不需要代理来访问外部网络,仅仅是访问 Docker Hub 过慢的话,还可以考虑 DaoCloud 提供的 Mirror加速器。具体参见:https://dashboard.daocloud.io/mirror

使用 Docker Machine 取代 Boot2Docker 在 OSX 中使用 Docker

原先,我们使用Boot2Docker在OSX下管理Docker。在Docker发布了 Toolbox 之后,新增了 Docker Machine 命令行。通过 docker-machine ,可以让我们以一致化的命令在本地或者云计算上创建 docker 服务。同时很方便的切换 docker 命令对应的目标服务器。

一、安装 Docker Toolbox

Docker Toolbox 安装包包含了:
docker 客户端
docker-machine 工具
docker-compose 工具
Kitematic 一个Docker的GUI

我们可以在 官网 或者 DaoCloud 镜像 下载并安装。

二、将Boot2Docker迁移到Docker Machine

如果原先并没有使用Boot2Docker,可以直接忽略

使用命令:
docker-machine create -d virtualbox –virtualbox-import-boot2docker-vm boot2docker-vm dev
-d virtualbox
使用virtualbox驱动
–virtualbox-import-boot2docker-vm boot2docker-vm
导入virtualbox中boot2docker的vm boot2docker原先在virtualbox创建的虚拟机名字为 boot2docker-vm
dev
可以根据自己需要设置 docker-machine 中的机器名

三、Docker Machine 使用

1. 创建一台新的 Docker Machine

docker-machine create –driver virtualbox default
–driver virtualbox
本机使用的话 使用virtualbox驱动
driver 还可以是amazonec2, azure, digitalocean, exoscale, generic, google, openstack, rackspace, softlayer, virtualbox, vmwarefusion, vmwarevcloudair, vmwarevsphere 等 配置对应的秘钥和参数后 可以直接在云服务中创建设备
default
docker-machine 中的机器名 之后的管理命令均使用该名称

2. 设置环境变量

在创建好 Docker Machine 之后,需要让本地的 docker 客户端了解需要连接的 docker 目标服务器地址,或者在不同的docker服务器之间切换,因此需要设置环境变量。

eval “$(docker-machine env default)”
–driver virtualbox
default
之前创建的machine的名称

3. 使用docker客户端管理docker

当我们将环境变量配置完成之后,我们就可以在本地操作docker一下,使用docker命令管理docker设备了

三、Docker Machine 管理

docker machine 提供了一系列命令来管理对应的docker设备

以下是 boot2docker 与 docker-machine 命令的对照表 使用 docker-machine 时 最后都需要跟上对应的设备名称

boot2docker docker-machine docker-machine description
init create 创建新的docker主机
up start 启动一个关闭的主机
ssh ssh 通过ssh连接到对应的docker主机
save –
down stop 关闭正在运行时的主机
poweroff stop 关闭正在运行时的主机
reset restart 重启正在运行的主机
config inspect 获取主机的参数配置信息
status ls 列出所有主机及其状态
info inspect 获取主机的参数配置信息
ip ip 获取主机的IP地址
shellinit env 设置主机的环境变量
delete rm 删除主机
download –
upgrade upgrade 将主机的docker客户端更新到最新版本

 

 

记一次UnitedStack(有云)平台故障

最近各种云平台事故不断,然而作为喜欢尝试国内各种云的我,也躺枪不断,先是赶上QingCloud的广东机房(广东睿江科技)遭雷劈导致业务中断[故障报告],其次是QingCloud北京2区的网络故障[故障报告],接着是七牛的域名被Godaddy强行clientHold导致的业务中断[故障报告],同时关于七牛还踩到老用户的坑,使得影响非常糟糕,这个有机会另外说。然而这些故障基本都是在工作日的白天出现的,虽然部分恶性故障拖的时间都比较久,对业务影响还是不小,但是也因为用户基数大,同时官方跟进和报告也相对及时,客服也都跟进的比较快,使得问题处理还是相对透明。虽然有人要说「我们要给云计算多一些宽容」但是在使用云服务时候,我们自身也应该做好监控和容灾,鸡蛋都放在一个碗里,迟早还是要出事的。

而下面记述的就是一个并不大的云服务商 UnitedStack(有云)的一次平台故障经历。分为故障线和客服线两条线进行记述。

2015-08-05 01:35
服务器无法访问,ping不通,但是管理平台显示机器正常运行

2015-08-05 01:40
尝试通过管理平台进行重启操作,机器显示正在关机中

2015-08-05 02:10
经过了20分钟管理平台依旧显示关机中,因此提交工单,请求检查机器问题

2015-08-05 02:31
管理平台依旧显示关机中,工单无人响应,尝试联系客服电话,均无人接听,客服电话选择投诉时,接听人表示已经离职。

2015-08-05 02:38
管理平台显示运行中(表示已经重启),但不管管理面板的VNC还是通过网络都无法连通主机。
管理平台本身的其他操作均表现异常

2015-08-05 03:02
主机恢复连通,并且运行正常,检查主机上的业务本身运行也恢复正常

2015-08-05 11:03
技术人员回复工单表示在检查问题

2015-08-05 11:32
技术人员表示多次测试并无发现问题

2015-08-05 11:42
我重新阐述故障期间的具体情况,要求技术人员协助排查问题

2015-08-05 15:03
技术人员答复需要我提供故障主机的相关日志,包括boot.log、dmesg、messages日志等。

2015-08-05 17:24
我打包上传上述三个日志

2015-08-05 19:20
技术人员答复 检查到在故障时间段里,其服务器的连通上出现了问题,造成相关服务异常,目前正在进一步处理中。

2015-08-13 15:53
技术人员答复如下:

我方查到故障期间dell核心交换机出现问题(内存报错),导致我方服务连通出现问题。dell方面也未能查出最终原因。
目前我方正在部署监控,如果故障再次出现,我方可以及时人工干预处理,避免造成更大影响。

整个事件:

故障持续事件:1个半小时,因为在夜间,甚至说如果我完全不管正在睡觉的话,如果没有监控报告根本察觉不到,故障本身影响在可控范围。但是这已经与其在官网上承诺的99.95%的SLA不符(以自然月为单位,不可用时间不超过21分钟),同时在故障期间,工单与客服电话均无人响应,同时初步检查结果并未发现问题所在,故障整理排查与给出答复耗时长达一周多。
UnitedStack SLA

虽然说把这个记述下来并不是表示不满,主要还是希望表达,云计算即使是很复杂的体系,而现在云计算方面的创业公司很多,虽然我们应该多给他们一些信任和支持,但是在出现问题时,对于技术人员值班的问题,以及对于故障的监控上也确实暴露出不少问题,而这些对于传统IDC来说都是基本功的东西,却在云计算服务上确实了实属不应该。

Linux下木马病毒清理小记

昨天下午接到朋友的电话,说其一台业务服务器被机房告知因为大量Out Bound流量,被断网要求清查。因为急于恢复业务,因此需要先进行清理保证机器不再出现恶意流量,恢复网络然后再进行进一步处理。估将主要清理情况记录如下:

首先,登录机器后,发现晨间存在异地登录行为:

last

随后查询历史记录,发现下载并执行了5个恶意的程序,并且添加了启动项

history

5个恶意程序均为二进制文件,尝试将上述5个文件kill,并且删除,并且清除rc.local启动项,但发现程序依旧会继续创建并运行(要那么容易被干掉也就算不上木马了),因此决定进行全盘排查。

首先根据目录的修改时间(万幸病毒并没有篡改自身创建时间),将问题锁定到了下面几个文件(目录):

首先/bin 目录,除开下载的三个文件之外,病毒还用自身替换了 ps 文件 与 netstat 文件,同时创建了数个用于计划任务的启动脚本。(可以预见中了病毒,常用的系统命令早已并不可靠,不但无法用于排查问题,反而会重启病毒)

其次是 /usr/sbin 下的 lsof 与 ss 文件也被替换成了病毒本体。

接着是/etc目录的 crontab 增加了一条计划任务 用于定时重启病毒进程(死灰复燃)
同样/etc/rc.d中的 init.d / rc1.d / rc2.d / rc3.d / rc4.d / rc5.d 下也均增加了病毒的执行脚本
并且取了其实挺显眼的名字 DbSecuritySpt 和 selinux 。

最后在 /usr/bin 目录下面存有一个目录保存有病毒的副本(用于计划任务启动)。

通过各个部分环环相扣,保证任何一个部分即使被删除,或者停止,依旧能够死灰复燃。

清理完成,同时恢复被病毒替换掉的系统文件,把SSH的密码登陆干掉,经过网管监控数小时没有出现异常流量之后,算是暂告一段落。当然,接下来恢复还是迁移业务啊,什么的,就让朋友自己折腾了。

当然其实还是那句话,SSH要改端口,不要用密码登陆啊 →_→

对于上述程序有兴趣的欢迎下载:
链接: 度盘 密码: s9pe