创建 redis systemd 服务

服务器操作系统一直使用的是 ubuntu server,因为安装软件很方便,直接apt-get install就行。但使用 apt 安装 redis 版本比较旧,也没有新的源,所有只能自己编译安装了。不过有个问题,自己编译安装的启动和重启比较麻烦(启动:/usr/bin/redis-server /etc/redis/redis.conf,停止:/usr/bin/redis-cli shutdown)。我在想能不能使用类似 ubuntu service xxx restart 的指令。

搜索了一下发现可以使用upstart来实现这个需求,但是这个在 ubuntu 15.04 已经废弃了,推荐使用更先进的systemd。在 /etc/systemd/system/ 下创建一个 redis.service:

其中主要是定义了启动用户,启动命令,停止命令。有个设置比较重要Type=forking,可以理解为 systemd 是一个类似 supervise 的守护进程,forking 表示服务管理器是系统 init 的子进程,用于管理需要后台运行的服务。同时还需要修改 redis 的配置:

然后就可以使用sudo systemctl start redis命令启动 redis 了。同时支持 stop、restart 等常用指令。启动成功后运行 sudo systemctl status redis查看运行状态:

可以看见 redis 已经正常运行了。可以尝试使用 kill -9 命令把 redis 杀死,你会发现 redis 马上又会被拉起,这样能保证服务能一直运行(当然若是服务有问题一直蹦,谁也救不了你)。若需要开机启动还可以运行sudo systemctl enable redis命令加入到开启启动项里面。

我们平常自己写的程序也可以依样画葫芦写个配置文件,使用 systemctl 来做服务管理,这种方式更加先进,还有一些其它实用的功能。

 

参考资料:

Ubuntu systemd service file for Redis, https://gist.github.com/geschke/ab6afa91b2d9dfcd5c25

systemd – ArchWiki, https://wiki.archlinux.org/index.php/Systemd

How To Install and Configure Redis on Ubuntu 16.04 | DigitalOcean, https://www.digitalocean.com/community/tutorials/how-to-install-and-configure-redis-on-ubuntu-16-04

Ubuntu 16.04 上安装 MySQL 5.7 的一些细节

最近腾讯云折扣力度比较大,在加上之前 DigitalOcean 上的 VPS 访问不是太顺畅,就把服务器迁回了国内。重装了一把 MySQL,遇到了一些细节上的问题,觉得很有意义,记录下。

首先使用 MySQL 官方的源安装,这个在一年前的《Ubuntu 平滑升级 MySQL 到 5.7》中描述过,具体过程不再赘述。说一些细节上的东西:

1. Ubuntu 16.04 要求 2017.01.01 后所有的源必须使用 SHA2 以上版本的摘要签名算法(https://wiki.debian.org/Teams/Apt/Sha1Removal)。目前来说具体表现是在执行sudo apt-get update时会显示一个警告“W: http://repo.mysql.com/apt/ubuntu/dists/xenial/InRelease: Signature by key A4A9406876FCBD3C456770C88C718D3B5072E1F5 uses weak digest algorithm (SHA1)”。访问一下网址确实看见使用的是 SHA1 签名,这个只能等待官方修复。

2. 安装过程中会要求设置 root 密码,这里你可以你直接回车(设置空密码),但实际上是给你创建了一个 auth_socket 的 root 用户,并不是空密码。

这时你登录的话需要使用 socket 方式连入:sudo mysql --protocol=socket --socket=/var/run/mysqld/mysqld.sock,注意这里需要用 root 权限运行,保证有足够权限读取 sock 文件,不然会默认使用当前用户进行传统的 ip+port 登录,这样肯定是登录不上的。登录上去就可以改 root 密码了,正常执行ALTER USER 'root'@'localhost' IDENTIFIED BY 'test';后发现密码并没有被改变(会报 warning),观察上面的截图会发现 root 账号的认证插件用的是auth_socket,我们需要这样执行命令ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'test';。就能回到传统的账号密码认证了。

3. 细心的同学可能已经发现了,多了一个mysql.sys的用户,按照官方的说法这是给 DBA 使用的,防止直接使用 root 账号带来的重命名和误删账号的问题。这个用户默认有一个初始密码,但在日志里面没找到(源码编译 MySQL 5.7 后 mysql_install_db –initialize 时会把密码记录到日志中)。这里索性重新设置一个密码(ALTER USER ‘mysql.sys’@’localhost’ IDENTIFIED BY ‘test’;)。

到这儿基本上已经安装完成了,但若你细心的话会发现在启动日志里面会有一些警告:

1. TIMESTAMP with implicit DEFAULT value is deprecated. Please use –explicit_defaults_for_timestamp server option (see documentation for more details). 这个是说explicit_defaults_for_timestamp缺省值已经废弃了,需要显式的在 my.cnf 设置 explicit_defaults_for_timestamp = 1

2. [Warning] Could not increase number of max_open_files to more than 5000 (request: 65535) 这是个常见的错误,一般修改 /etc/security/limits.conf 即可,但改了之后好像不奏效。这是因为在 /lib/systemd/system/mysql.service 配置中还有参数 LimitNOFILE 限制,把这里也修改了就可以了。

3. 还有日志中时间的时区和系统的不一致,看日志不方便。这是 5.7 新加的参数log_timestampshttp://dev.mysql.com/doc/refman/5.7/en/server-system-variables.html#sysvar_log_timestamps),在 my.cnf 设置为 log_timestamps = SYSTEM 即可。

全部搞定后建议执行sudo mysql_secure_installation命令,做一些安全设置,比如禁止root远程登录、删除匿名用户、验证密码安全级别等。

最后介绍一个比较好用的命令:mysql_config_editor,解决命令行登录MySQL便捷性和安全性的问题。我们为了本地登录MySQL方便经常会写一些一键登录的小脚本。基本上都是直接执行 mysql -uroot -p’test’ 命令,这样会带来一个问题,执行的命令会被记录在bash_history中,当然你也可以禁用bash_history的功能,但这样会失去一些便捷性,也不大方便追查历史的问题命令。而且在新版本中直接使用这种方式登录会有警告信息。

这时我们就可以使用 mysql_config_editor 来创建一个 login-path,登录时直接使用mysql --login-path=xxx即可。一个典型的创建指令:mysql_config_editor set –login-path=xxx –host=localhost –user=root –port=3306 –password,然后输入密码。这里需要注意一下,如果粘贴的密码中有特殊字符会有问题,需要加上引号。比如你的密码是 test!@#,需要复制文本 'test!@#' 进行粘贴。创建的文件默认在 ~/.mylogin.cnf,不过这是个二进制文件,看不出来啥(比较安全)。我们可以使用 mysql_config_editor print --all 命令来看看已经创建的 login-path:

这里也能看见密码是加密存储的,在一定程度上保证了安全。而且 .mylogin.cnf 文件可以分发给使用同一个MySQL-client的用户,提高了安全性也方便运维管理。很实用的功能,强烈推荐使用。

MySQL 每个版本都有很多安全性和性能的改进,值得去学习、研究。而且 MySQL 8(现在都流行跳版本)也快出来了,前景更是一片大好。

 

参考资料:

Change user password in MySQL 5.7 with “plugin: auth_socket”, https://www.percona.com/blog/2016/03/16/change-user-password-in-mysql-5-7-with-plugin-auth_socket/

Where is the MySQL 5.7 root password?, https://www.percona.com/blog/2016/05/18/where-is-the-mysql-5-7-root-password/

Do I need to change the mysql.sys password, http://askubuntu.com/questions/824139/do-i-need-to-change-the-mysql-sys-password

Can not increase max_open_files for Mysql max-connections in Ubuntu 15, http://stackoverflow.com/questions/30901041/can-not-increase-max-open-files-for-mysql-max-connections-in-ubuntu-15

使用karma+mocha进行前端测试驱动开发(一)

之前有接触过一些使用 casperJs 做前端自动化测试和页面元素监控,效果还不错,这个后续有时间再介绍一下。但对前端代码层面的测试不是很了解,今天下班比较早,闲来无事,就看了下最近听说的 karma+mocha 前端测试框架,发现还是很有意思,收(刚)获(刚)颇(入)多(门)。目前来看 karma+mocha 非常适合做 TDD(测试驱动开发)。

测试驱动开发(英语:Test-driven development,缩写为 TDD)是一种软件开发过程中的应用方法,由极限编程中倡导,以其倡导先写测试程序,然后编码实现其功能得名。

TDD 提倡写代码之前先把单测写好,然后通过单测来验证代码是否正确。这就需要有一个单测框架(即 mocha),但这还不够,也不能每写完一次就运行一下单测代码,这也效率太低了吧,所以还需要一个框架(即 karma)来做这个事情。

这里先介绍一些 JS 单测框架 mocha(https://github.com/mochajs/mochatj 大神的作品,奈何大神觉得 Node 没搞头去搞 go 了)。安装还是比较简单,这里就不多说了。提醒一下,如果是在 Windows 下安装 Node 的话可能需要设置一下环境变量

实例:一个加法模块src/add.js的代码:

各个语言写单测都差不多,就是对预期结果的断言。上面代码对应的测试代码tests/add.test.js

测试脚本里面应该包括一个或多个describe块,每个describe块应该包括一个或多个it块。

describe块称为“测试套件”(test suite),表示一组相关的测试。它是一个函数,第一个参数是测试套件的名称(“加法函数的测试”),第二个参数是一个实际执行的函数。

it块称为“测试用例”(test case),表示一个单独的测试,是测试的最小单位。它也是一个函数,第一个参数是测试用例的名称(“1 加 1 应该等于 2”),第二个参数是一个实际执行的函数。

断言库有很多种,mocha 并不限制使用哪一种。上面代码引入的断言库是 chai,并且指定使用它的assert断言风格。因为这更接近其它语言的风格。

运行一下测试代码:

filehelper_1472143702070_9

大概流程就是这样,详细的可以查看官方文档。随便说一下,WebStorm 和 PhpStorm 原生支持 mocha,设置一下即可右键运行时直接调用 mocha 来运行当前 JS 单测文件:

20160826005459

20160826005634

20160826005739

单测已经写好了,怎么能让测试驱动我开发呢?这时候就需要 karma(https://github.com/karma-runner/karma)登场了。如 github 主页上所说,Karma 不是一个测试框架,也不是一个断言库,仅仅启动一个 http server,并通过你熟知的测试框架生成运行测试的HTML。Karma 支持的测试框架非常多,如前文介绍,我们这里选择的是 mocha 测试框架和 chai 断言库。

首先安装 karma,然后使用karma init my.conf.js命令生成配置文件。生成文件的过程是交互式的,很友好,注意选择测试框架是 mocha。选错了也没关系,配置文件之后可以手动编辑,大概长这样:

比较重要的几个参数:

  • frameworks:我们这里需要 mocha 和 chai。框架必须预先通过 npm install karma-xxx --save-dev 命令安装好,支持的框架详见:https://www.npmjs.com/browse/keyword/karma-adapter
  • files:可以配置通配符把源代码和测试代码加载进来。
  • browsers:需要启动的浏览器
  • autoWatch:观察文件是否变动,如有变动则重新运行单测。

最后使用命令karma start my.conf.js运行即可。但这里我一直失败,遇到如下这些错误:

这是因为上文的代码使用的是node的require()语法,karma是在浏览器端运行的,当然不支持了。如果你的源代码使用的是 AMD、CMD 加载器,测试代码也要使用 requirejs 等框架加载源代码模块。这里我们将源代码和测试代码稍微改造一下就行了:

成功运行:

20160826014133

karma 可以启动不同的浏览器,所以还可以做兼容性测试。

我修改一下源代码,karma 能自动监听到文件变动,然后显示单测结果,这样我就能实时知道代码是否有问题。

karma

当然这只是 karma 的一个功能,还有很多实用功能等待后续学习。

参考资料:

测试框架 Mocha 实例教程,http://www.ruanyifeng.com/blog/2015/12/a-mocha-tutorial-of-examples.html

[JavaScript] Test Suit 4 – Test Runner – Karma,http://veck.logdown.com/posts/248538-javascript-test-suit-4-test-runner-karma

典型的用户注册页面短信验证逻辑漏洞

用户注册是一个网站基本的功能,做过 web 开发的基本上都写过这个功能,但很多漏洞都出在这个页面上面。这里先不讨论密码加密存储,传输安全等问题,就说说注册页面的功能逻辑。

2016-05-22_000624

如上图的新浪微博注册页面,一般的注册页面都需要填写账号、密码等信息,为了防止恶意注册,都还有一个验证码,这几乎是一个注册页面的标配了。现在有些注册页面会多一个手机验证,防止恶意注册的同时也变相完成了实名制的红头文件要求,但有时候问题就出在这里。

2016-05-18_020524

上图这个注册页面,中规中矩,看似没什么问题,填写一些信息后点击下一步:

2016-05-18_020915

很常见的操作,POST 请求将信息带到下一个页面(校验了图形验证码),跳过来的同时已经给手机发了短信验证码,到目前为止没什么问题,用户体验还不错。不过发现页面上还有一个重新发送的按钮,点击一下发现直接调用 /sendRegMsg 接口发送了短信,接口只接收一个手机号参数,无其它参数。这里就是第一个漏洞:短信发送接口无权限验证,可被恶意调用造成经济损失,还可被利用于“短信炸弹”,而且这家厂商本身就是提供短信验证码服务的,估计“短信炸弹”效果更改。要解决这个漏洞也很简单,加个验证码就行了,但这里可能又会有一个坑(后面细说)。实际短信验证码为 4 位,提交企业名称等信息时也没有图形验证码校验:

2016-05-18_021422

那就好办了,直接跑个小脚本暴力提交(最多9999次)即可:

2016-05-18_024823

这里就是第二个漏洞了,任意手机号注册(一大波薅羊毛党将要来袭):

2016-05-18_024610

这两个漏洞都是很好修复的,影响也不大,但若安全意识薄弱,逻辑没有理清楚会造成修复不彻底。比如在短信重新发送操作那里加了一个图形验证码,发送短信和提交时都会做校验,但若你调用一次短信发送接口后图形验证码没有重置,我就可以拿着这一个正确的图形验证码一直调用发送短信接口(重放攻击)也能达到恶意调用和“短信炸弹”的目的,提交信息时也可以用这一个的正确图形验证码暴力提交。还有的为了限制短信发送频率,点击发送短信后将发送按钮设置为 disabled="disabled",然后设置一个定时器 60s 后恢复,这完全也是自己骗自己。

一种比较好的做法是每发一次短信图形验证码都重置,短信验证码填写失败一定次数后重置校验 session,后面任何输入都返回错误。

其实这个漏洞很鸡肋也很低级,但目前这种逻辑漏洞已经成为主要的问题,看似低级,但很难避免,往往影响还很大。而且我们可以看到图形验证码在整个流程中扮演了很重要的角色,但你的图形验证码真的安全吗?答案是否定的,百度贴吧和 12306 “变态”的验证码都是几天内就被破解了,甚至还有人工打码平台,可谓是道高一尺魔高一丈。安全,还是仍重道远呀。

推荐阅读:

黑产揭秘:“打码平台”那点事儿 http://www.freebuf.com/articles/others-articles/104536.html

参考资料:

无(不要问为什么知道这类漏洞)

kanqiu_26461760_2144148040__20160126154003

配置Nginx,开启HTTP/2

HTTP/2(超文本传输协议第2版,最初命名为 HTTP 2.0),是在 Google 之前提出的 SPDY 协议的基础上演变而来,相对 HTTP1.1 增加了连接复用、头部压缩、服务端 push 等特性。与 HTTP1.1 完全语义兼容,几乎可以无缝升级。目前主流浏览器都已经支持 HTTP/2 了(IE 自 IE 11 开始支持)。

开源版本的 Nginx 从 1.9.5 版开始支持 HTTP/2,其实配置也很简单,升级 Nginx 到最新版本,然后把之前 HTTPS 配置中的 spdy 改成 http2 就行了(listen 443 ssl http2 default_server),但改了之后发现并没有生效,直接退回到了 HTTP1.1。nginx -V 查看编译参数也带有 --with-http_v2_module,应该没什么问题才对。Google 了一把发现是 OpenSSL 版本的问题,我使用的是 ubuntu 14.04 下的 ppa:nginx/stable 源,这个源上的 Nginx 是在 OpenSSL 1.0.1f 上编译而成的,而 OpenSSL 的这个版本不支持 ALPN,所以无法开启 HTTP/2。

Note that accepting HTTP/2 connections over TLS requires the “Application-Layer Protocol Negotiation” (ALPN) TLS extension support, which is available only since OpenSSL version 1.0.2. Using the “Next Protocol Negotiation” (NPN) TLS extension for this purpose (available since OpenSSL version 1.0.1) is not guaranteed.

知道原因就好办了,先升级本地的 OpenSSL到最新版:

然后替换 Nginx 源为 ppa:ondrej/nginx 重新安装 Nginx:

现在再打开网站,发现 HTTP/2 已经开启成功了。

2016-05-21_233215

当然如果你是 ubuntu 16.04,就没有那么麻烦了,直接安装 Nginx 配置一下就行:https://www.digitalocean.com/community/tutorials/how-to-set-up-nginx-with-http-2-support-on-ubuntu-16-04

推荐阅读:

Dropbox 迁移到 HTTP/2 的经验,满满都是干货:https://blogs.dropbox.com/tech/2016/05/enabling-http2-for-dropbox-web-services-experiences-and-observations/

检查网站是否支持 SPDY 或者 HTTP/2 的 Chrome 扩展:HTTP/2 and SPDY indicator

参考资料:

https://www.nginx.com/blog/nginx-1-9-5/

http://serverfault.com/questions/732474/nginx-configured-with-http2-doesnt-deliver-http-2

http://nginx.org/en/docs/http/ngx_http_v2_module.html

为什么我们应该尽快支持 ALPN?https://imququ.com/post/enable-alpn-asap.html

一个css选择器的小坑

之前博客主题中的有序列表显示有点问题,列表标记位置会在正文左边突出一部分,所以设置了 padding 值来解决此问题:

但有发现无序列表也有这个问题:

2016-05-10_015434

所以想当然的在之前的位置后面把 ul 标签加上:

然后就悲剧了,页面上其它位置的 ul 也 padding-left: 20px 了:

2016-05-10_020412

好久没写 css 了,百思不得其解,试过 .article-content > ol, ul.article-content section > ol, ul 都不行,甚至怀疑同级标签也有继承的关系。后来发现其它地方会把 class 标签写两次才恍然大悟,应该是逗号「,」优先级更高一点,把选择器解析成了 .article-content ol 和 ul 两部分,所以选择器写成 .article-content ol, .article-content ul 就可以了。

不用就会忘,随便改点东西就踩坑了。Orz!

PHP单元测试-mock和数据库测试

在计算机编程中,单元测试(英语:Unit Testing)又称为模块测试, 是针对程序模块(软件设计的最小单位)来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。在过程化编程中,一个单元就是单个程序、函数、过程等;对于面向对象编程,最小单元就是方法,包括基类(超类)、抽象类、或者派生类(子类)中的方法。

本文主要是根据PHPUnit(The PHP Testing Framework)文档结合实例简单介绍一下 PHP 单元测试中 mock 和数据库测试。如果你是初次接触单测的话建议先看一下 PHPUnit 文档中的入门章节

通常来说是开发程序和单测是同步进行的,项目提测的时候核心模块都需要包括单测(报告),但这个要求在不同的公司、部门、项目组要求不一样。虽然单测会占用一定的开发时间但总的来说单测是利远大于弊,最大的好处是自己或者别人后续更新模块功能时不用担心对原有功能造成了影响而不知情。

下面是一个具体的例子,在 web 开发中这样一个场景可能很常见:PHP 提供一个帐号注册的接口供前端调用,接口先检验一下此用户名是否已经存在,不存在的话插入数据库,返回注册成功。接口代码是这样的:

主要调用了 Join 类的 signIn 方法。我们来看看 Join 类是啥样:

逻辑很简单,先调用 Db 类的 exists 方法判断用户名是否存在,不存在的话使用 insert 方法插入数据。Join 类是这次业务新加的,比较重要,需要单测来保障质量,但这里用到了个 Db 类,这个库是以前就有的(坑),可能会影响本模块单测的正确性,而且 Db 类需要连接数据库,比较麻烦,这种场景就需要 mock 了。本文说的 mock 是广义上的,包括 Stubs(桩件)和仿件对象(Mock Object)。

将对象替换为(可选地)返回配置好的返回值的测试替身的实践方法称为上桩(stubbing)。可以用桩件(stub)来“替换掉被测系统所依赖的实际组件,这样测试就有了对被测系统的间接输入的控制点。这使得测试能强制安排被测系统的执行路径,否则被测系统可能无法执行”。

将对象替换为能验证预期行为(例如断言某个方法必会被调用)的测试替身的实践方法称为模仿(mocking)

我们这里应用的是打桩的概念。signIn 方法有两个分支:用户名存在和不存在。所以我们需要让 Db 类的 exists 方法在输入某个(些)用户名的时候返回 true。主要使用 PHPUnit_Framework_TestCase 类提供的 getMockBuilder() 方法来建立一个桩件对象:

代码看上去很像是实例化了一个类,其实原理也和这个差不多,PHPUnit 通过反射机制获取到类及其方法的信息,然后使用内置模板生成一个新类。我们需要 mock 掉 insertexists 方法:

这里使用了桩件生成器的 setMethods() 方法来设置哪些方法被上桩,以下是生成器提供的方法列表:

  • setMethods(array $methods) 可以在仿件生成器对象上调用,来指定哪些方法将被替换为可配置的测试替身。其他方法的行为不会有所改变。如果调用 setMethods(null),那么没有方法会被替换。
  • setConstructorArgs(array $args) 可用于向原版类的构造函数(默认情况下不会被替换为伪实现)提供参数数组。
  • setMockClassName($name) 可用于指定生成的测试替身类的类名。
  • disableOriginalConstructor() 参数可用于禁用对原版类的构造方法的调用。
  • disableOriginalClone() 可用于禁用对原版类的克隆方法的调用。
  • disableAutoload()可用于在测试替身类的生成期间禁用 __autoload()

然后分别设置两个方法的参数和返回值。这里 insert 操作比较简单,可以用 willReturn($value) 返回简单值:

上面的例子中,使用了 willReturn($value) 返回简单值。这个简短的语法相当于 will($this->returnValue($value))。而在这个长点的语法中,可以使用变量,从而实现更复杂的上桩行为。我们这里的需求是需要根据预定义的参数清单来返回不同的值,显然这是一个映射(map),PHPUnit 提供现成的 returnValueMap() 方法来做这个事情:

完整的单测代码: Continue Reading…

【译】简单介绍通过Predis库在PHP使用Redis

本文翻译自 《An Introduction to Redis in PHP using Predis》,已得到作者 @Daniel Gafitescu 的允许。原文版权归作者和 sitepoint.com 所有,如果你有原文使用需求请自行联系作者。

Redis 是一个开源的内存数据库服务器,得益于内建的数据类型,Redis 不仅仅只能做简单的 key/value 存储。

Redis 由 @Salvatore Sanfilippo 在 2009 年发布,因为其受欢迎和增长迅速,被很多大公司比如 VMware(后来聘请作者去全职工作)、GitHub、Craigslist、Disqus、Digg、Blizzard、Instagram 等(详见:redis.io/topics/whos-using-redis)使用。

你可以使用 Redis 做会话(session)处理程序,当你使用负载均衡的分布式服务时特别有用。Redis 也可以用作发布/订阅系统,很优雅的创建一个在线聊天或者实时订购程序。关于 Redis 的文档,所有的命令以及其它信息都能在项目网站 redis.io 上找到。

一直以来都有 Redis 和 Memcache 哪个更好的争论,文章 as the benchmarks show 显示在相同的基础操作上两者不相上下。Redis 比 Memcache 有更多的特性,比如内存存储、磁盘持久化、原子操作、事务以及不用记录每一次变化到磁盘上而是用服务端的数据结构来代替。

在这篇文章中将介绍如何使用 Predis 库所提供的一部分基础但很有用的 Redis 命令。

容易安装

Redis 容易安装,简明的安装说明发表在产品的下载页。从我个人经验来看,如果你运行在 Ubuntu 上而又没有安装 TCL(只需运行 sudo apt-get install tcl 即可安装)将会报错。一旦 Redis 安装完成,你可以运行服务:

Redis 的网站上显示的有很多语言可用的 Redis 客户端,每种语言都有好几个。对于 PHP 来说有 5 个。在本文中我使用的是 Predis 库,但是你可能也需要作为 PHP 模块编译、安装的 phpredis 扩展

译者注:有些人可能在 Predis 库和 phpredis 扩展中难于选择,但两者在一般场景下相差不大,目前都支持 PHP7。phpredis 扩展在性能上可能有一些优势,而 Predis 库源码更加优雅(适合学习、阅读),支持 PHP 新的语法特征,但文档较少(这也是我翻译这篇文章的主要原因)。还有,Predis 也有扩展支持(但作者好像没精力维护了,目前还不支持 PHP7),用于提高性能和提供一下其它的特性。

如果你向我一样在机器上安装的有 Git,你只需克隆 Predis 仓库。否则你就要下载 ZIP 包然后解压。(译者注:可以使用更加简便的 composer 安装:composer require predis/predis

测试一下,创建一个如下内容的 test.php 文件,测试是否能通过 Predis 连接上运行着的 Redis。

当你运行这个脚本,你应该能如愿的看见(输出)信息:”Successfully connected to Redis”。

译者注:这里可能有点小问题,代码不会按照预期捕获 Redis 连接失败的异常,因为在初始化 Redis 客户端类的时候并没有真正的连接,而是在运行第一个命令时才做连接,所以需要额外运行一下 $redis->connect(); 来触发连接操作。详见:https://github.com/nrk/predis/issues/61

使用 Redis

在这个章节你将了解众多 Redis 提供的命令的概况。Memcache 也有相似的命令,如果你熟悉 Memcache,这些列表对你来说看起来都不会陌生。 Continue Reading…

Nginx+PHP-FPM 500 504错误简析

最近新配置的 Nginx + PHP-FPM 环境遇到了几次 Nginx 500(Internal Server Error) 和 504(gateway timeout) 错误。就索性把配置修改的过程记录下来,已备后查。

先说一下 500 错误,这个一般是 PHP 代码错了,看一下 PHP 的 error 日志就清楚了。但有时候是 Nginx 和 PHP-FPM 配置不当导致的,比如说上传文件的场景。我们都知道 php.ini 文件里面 post_max_size 和 upload_max_filesize 两个配置项决定了文件上传的大小。但上调配置后发现大文件上传还是会 500,原来是因为 Nginx 也会限制上传文件的大小,有 client_max_body_size 配置项决定。这里还有一个比较重要的参数:client_body_buffer_size。为了节省资源 Nginx 不会把 request body 里面的数据一口气读到内存中,而是分片处理,这个参数就是设置缓存区的大小(一般也不用改),大于缓冲区大小的数据会以一个个小文件(buffer_size)形式存储到 client_body_temp_path,为了避免高并发下缓存文件冲突和一些文件系统限制(比如 ext3 磁盘格式文件夹下一级文件个数限制为 3.2w 个)以及提高性能可根据业务需要设置多级子目录。还需特别注意的是路径要有被 Nginx 进程写的权限,不然又会 500 错误了。

然后是 504 错误,除了业务代码本身超时更多是配置问题。还是先说 PHP 的配置,很多人都知道 PHP 脚本有执行时间限制(php.ini 中的 max_execution_time),但在 PHP-FPM 却是 php-fpm.conf 中的 request_terminate_timeout 配置项控制的。还有就是 max_input_time 配置项,值比较小的话在接收大文件或大数据量 POST 请求的时候可能会超时,一般不用做改动,因为大部分场景都是上游 web server 接收好数据后传给 PHP-FPM 进程的,传输的非常快,几乎不会超时,但也不能一概而论,要看具体业务场景。然后就是 Nginx 的锅了,刚也说到是 Nginx 接收好(或者说是转发)数据后传给下游,在这个承上启下的环节很容易超时。几个比较重要的配置项:fastcgi_send_timeout 控制向下游(这里是 PHP-FPM)发送数据的超时时间,与之对应的一个配置项是 fastcgi_read_timeout,表示读取下游 response 的超时时间,这里需要注意的是要和前面说的 request_terminate_timeout 保持一致(或大于)。还有一个配置项 fastcgi_connect_timeout,表示和下游 FastCGI 建立连接的超时时间,不过这个值就不用设置的太长了,不然下游某个节点挂了就会夯住 Nginx 的处理线程,影响业务。

我所遇到的场景就是上面这些,可能还有其它一些情况,具体问题具体分析应该也很好解决。

参考资料:

http://nginx.org/en/docs/http/ngx_http_core_module.html

http://nginx.org/en/docs/http/ngx_http_fastcgi_module.html

《PHP-CGI 进程 CPU 100% 与 file_get_contents 函数的关系》 http://zyan.cc/tags/request_terminate_timeout/1/

《PHP超时处理全面总结》 https://segmentfault.com/a/1190000000313184

《PHP file upload affected or not by max_input_time?》 http://stackoverflow.com/questions/11387113/php-file-upload-affected-or-not-by-max-input-time

一个 MySQL 用户名长度的坑

今天使用 PHP 连接一个 MySQL 数据库的时候连不上,提示无权限。

因为 MySQL 是在另外一个机房,首先想到的是防火墙的原因,但使用 MySQL-cli 却能正常连接,遂排除这种可能。

又怀疑是 PHP 框架的问题,写了一个简单的测试脚本,主要语句:mysqli_connect(),并打印出错误。运行一下还是不行,错误如下:

真是奇了怪了,这个用户创建时指定的是通配符 ‘%’,而且在别的机器都可以连接。又把测试脚本放在以前一直运行的环境(PHP 7)中,能运行通过。

看来是是环境问题(PHP 5.4)了,回过头来看那句报错,发现用户名好像被截断了(应该是xxx_user),是不是显示的问题,随便改一个用户名试试,同样报错,用户名却没有截断。这时又想了想是不是 MySQL 的版本太高(5.7.10)了。找了个 MySQL 5.5 的环境,创建相同的用户却发现报错了:String ‘xxx_user’ is too long for user name (should be no longer than 16)。查询 MySQL 文档发现:MySQL user names can be up to 32 characters long (16 characters before MySQL 5.7.8). http://dev.mysql.com/doc/refman/5.7/en/user-names.html

这样看来应该是老版 PHP 的 mysqli 扩展内部限定了用户名的长度,但新版的 MySQL 却可以创建更长的用户名了。知道原因了就很好办了,创建一个短用户名 OK 了。其实也是阴差阳错,因为新库是多应用共有,所以用户名创建的比较长。_(┐「ε:)_