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 下也均增加了病毒的执行脚本
并且取了其实挺显眼的名字 DbSecuritySptselinux

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

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

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

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

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