标签 PHP 下的文章

PS: 此方案适用于 Windows、macOS

老祖宗说过磨刀不误砍柴工,又说过工欲善其事,必先利其器。这话无论放到何时都适用。上次折腾开发环境是 Docker 优化之 Docker-sync 解决 Docker 挂载缓慢 的问题,然而这一改问题更大了。

在我日常开发了数天后,总结了 docker-sync 的诸多问题:

  1. 宿主机修改时而不同步,这个在文章中有讲过,怀疑和内存/运行时间有关系
  2. 如果项目过大,start 命令的同步时间过长,这通常需要 10~20 分钟

试想,问题 1 和 2 通常是成对出现的,也就是说只要发现文件不同步,那可就一直不同步了,我曾认为同步是监听文件的修改事件来通知更新,然而 CTRL + S 按烂也没有反应。接着能做的只有清除掉同步的数据然后重新同步。[狂汗]

这通常折腾你两次你可能就很难受了,重新同步完你刚刚做的什么也忘得七七八八了,谁曾想装个同步工具反倒同步出了问题。[无奈]

上篇文章有提到,挂载速度损耗最小的是利用虚拟机挂载。

环境结构

简单描述这几套开发环境的结构。

只使用 Docker:本地文件 --挂载--> Docker 容器 --映射端口--> 宿主机

只使用 Vagrant:本地文件 --挂载--> 虚拟机 --映射端口--> 宿主机

Docker+Vagrant:本地文件 --挂载--> 虚拟机 --挂载--> Docker --映射端口--> 虚拟机 --映射端口--> 宿主机

安装

首先需要安装 Vagrant 和 VirtualBox 这两个程序。

选择你系统对应的最新版本,安装步骤不用细细的讲。

导入镜像

# 创建 vagrant 镜像配置目录
$ mkdir ~/vagrant
# 拉取 centos 镜像
$ cd ~/vagrant
$ vagrant init centos/7

通过 vagrant init 命令拉取镜像在国内速度很慢,你可以配置代理来加速拉取,前提是需要先启用代理。

$ export http_proxy=http://0.0.0.0:1087
$ export https_proxy=http://0.0.0.0:1087

或者你可以自己下载镜像,通过命令来手动导入镜像。

// 复制镜像到 vagrant 目录
$ cp /path/to/centos-7.box ~/vagrant
$ vagrant box add centos-7.box
$ vagrant init centos-7

配置

当镜像导入成功后,~/vagrant 目录将生成一个 Vagrantfile 你可以编辑它用于配置挂载的目录,映射的端口等。

# 映射端口
config.vm.network "forwarded_port", guest: 80, host: 80
config.vm.network "forwarded_port", guest: 80, host: 80, host_ip: "127.0.0.1"
# 挂载目录 忽略挂载 vagrant 目录
config.vm.synced_folder ".", "/vagrant", disabled: true
config.vm.synced_folder "~/dnmp", "/root/dnmp"
config.vm.synced_folder "~/Project", "/root/Project"

端口映射 80 和 3306 等,目录挂载我这里挂载了 ~/dnmp 目录,这个是一个类似 laradock/laradock 的项目,配置了我日常的开发环境,包括 PHP、Nginx、Redis、MySQL 等,使用 Docker 能快速构建我的开发环境。

安装 Docker

Vagrantfile 配置完成,便可以使用 vagrant up 命令启动虚拟机。但注意,关于 vagrant 相关的命令,你只能在 ~/vagrant 目录下才能执行,它依赖 Vagrantfile 文件。

$ cd ~/vgrant
# 开启虚拟机
$ vagrant up
...
# 登录到虚拟机
$ vagrant ssh
...
# 默认登录是 vagrant 用户,手动切换到 root 用户
$ sudo su -

至此虚拟机部分已经安装完成,后续如果调整 Vagrantfile 后需要执行 vagrant reload --provision 命令。

后续操作全部在虚拟机(使用 vagrant ssh 登录)中进行,然后准备 Docker 安装的必要的工具以及 Docker 的软件源。

$ sudo yum install -y yum-utils device-mapper-persistent-data lvm2
# 添加 docker 的软件源
$ sudo yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
# 更新 yum 缓存
$ sudo yum makecache fast
# 安装 docker-ce 版本
$ sudo yum -y install docker-ce
...
# 安装完成后启动 docker 的后台程序
$ sudo systemctl start docker

安装完成后,你可以使用 docker -v 命令查看当前安装的 Docker 版本。

优化 Docker

首先需要修改 Docker 的镜像地址,大陆使用 Docker 官方镜像速度令人发指,这里我使用阿里云提供的镜像加速服务。你可以打开登录阿里云的容器镜像服务来获取加速地址。地址:容器镜像服务

然后复制上你的加速地址,并打开 Docker 的配置文件。

$ vi /etc/docker/daemon.json

增加配置文件,并复制替换加速地址,格式如下:

{
    "registry-mirrors": ["https://xxxxxx.mirror.aliyuncs.com"]
}

配置完成后,需要重启 Docker 的镜像来加载加速地址。

sudo systemctl daemon-reload
sudo systemctl restart docker

构建开发环境

我这里使用的 yeszao/dnmp,我 fork 了一个分支到我的仓库并做了一些调整,地址:isecret/dnmp

本地我将仓库 clone~/dnmp 目录,并挂载到虚拟机的 /root/dnmp 目录,后续的操作使用同一套配置。

关于 dnmp 的操作可以查看对应的 README.md 来获得帮助。

注意事项

MySQL 无法启动,对应目录不可写入?

我曾在环境配置完成的时候发现 MySQL 拉不起来,查看日志发现对应目录不可写 [喷血],在查找相关文档的时候找到了 Docker 官方仓库的 Issues,地址:mysqld: Can't create/write to file '/var/lib/mysql/is_writable' (Errcode: 13 - Permission denied) #219,翻译下原文,这种情况通常在 macOS 和 Windows 挂载会出现,Docker 容器默认会以 mysql 用户运行,而挂载的文件则是 root 用户的 UID,需要指定容器的用户为 docker 用户的 UID 1000:50

配置文件如下:

mysql:
    image: mysql:${MYSQL_VERSION}
    user: "1000:50"
    restart: always
    environment:
      - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
    ports:
      - "${MYSQL_HOST_PORT}:3306"
    volumes:
      - ${MYSQL_CONF_FILE}:/etc/mysql/conf.d/mysql.cnf:ro
      - ${MYSQL_DATA_DIR}:/var/lib/mysql/:rw
    command: --innodb-use-native-aio=0
    networks:
      - default

修改完成后,需要重新 bulid 容器。

主页

主页:https://comments.hk/

文档:https://github.com/isecret/yuncun/blob/master/DOC.md

PS:首页定时 10s 自动更新

关于乐评

人这辈子,最害怕突然把某一首歌听懂了。 ——来自 北风神75 在「有多少爱可以重来」的评论

项目灵感来源于网易云音乐的与农夫山泉合作的乐瓶营销「乐瓶」——这 30 条乐评,是从网易云音乐后台点赞数最高的 8000 条乐评中,经过人工筛选产生的,它们文字简练,富有故事性,即使脱离歌曲本身也可以被理解。

在使用网易云音乐的时候,常常在评论区看到与之共鸣的评论。有时候很想将其记录下来,同朋友分享。时间久了,那种感动依然不可褪去。

你能在这倾听别人的故事,亦或许是你的故事。

我们能提供什么

截止目前,已拉取热歌排行榜 TOP199 的热门评论,共计 2984 条热门评论。

项目后台定期拉取热门歌曲排行榜列表并获取其中的热门评论,通过接口随机分发一条热门评论,你可以查看 API 文档 快速接入。

当然,你也可以通过提交歌曲或歌单 ID 来完善这个项目,将你的感动带给更多的人。

API 文档

JSON 格式

请求地址:https://api.comments.hk/

请求方式:GET

请求参数: 暂无

返回类型:JSON

返回参数:

参数名含义
song_id歌曲 ID
title歌曲名称
images歌曲封面图片,已处理为 https 链接
author歌曲作者
album歌曲所属专辑
description歌曲描述
pub_date歌曲发行时间
comment_id评论 ID
comment_user_id评论所属用户 ID
comment_nickname评论所属用户名称
comment_avatar_url评论所属用户头像链接,已处理为 https 链接
comment_content评论正文
comment_pub_date评论发表日期

示例

<script>
  var xhr = new XMLHttpRequest();
  xhr.open('get', 'https://api.comments.hk/');
  xhr.onreadystatechange = function () {
    if (xhr.readyState === 4) {
      var data = JSON.parse(xhr.responseText);
      var hotComments = document.getElementById('hotComments');
      hotComments.innerText = data.comment_content;
    }
  }
  xhr.send();
</script>

更新日志

  • 2019/4/21 更新 JSON 文档第一版

感谢

数据来源

项目歌曲数据、图像和评论数据来源于网易云音乐,网易云音乐对其拥有内容、商标所有权。

大概半年前,我将本地的开发环境换成了 Docker 来管理。也写了一套具有雏形的管理脚本,不过因为各种各样的问题,时不时需要去修改脚本甚是麻烦,最近选择将它换为 yeszao/dnmp 提供的脚本,内置 Nginx、PHP7/PHP5、MySQL、Redis。拥有丰富的配置项和标准的拓展。很是不错,不过我所开发的项目中有 yaf 拓展在这个脚本中没有实现,所以我 fork 了项目并添加了自己所需要的拓展,项目地址 isecret/dnmp,分支为 own,意为自己的分支,因为 yaf 拓展并非大众需求也就没有提交合并,就当做提供一个自己添加拓展的方法吧。

DNMP 同时内置了 XDebug,我在配置的时候遇到了坑,在此记录一下。XDebug 默认开启,配置文件在项目目录 conf/php.ini 末尾,默认配置如下。

[XDebug]
; Debug Config
xdebug.remote_enable = 1
xdebug.remote_handler = "dbgp"
xdebug.remote_connect_back = 1
xdebug.remote_port = 9000
;xdebug.remote_log = "/var/log/dnmp/php.xdebug.log"

然而我在 VS Code 中配置好 launch.json 后,尝试断点却始终无法正常工作。

{
    // 使用 IntelliSense 了解相关属性。 
    // 悬停以查看现有属性的描述。
    // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Listen for XDebug",
            "type": "php",
            "request": "launch",
            "pathMappings": {
                "/var/www/html/project_name": "${workspaceRoot}"
            },
            "port": 9000
        },
        {
            "name": "Launch currently open script",
            "type": "php",
            "request": "launch",
            "program": "${file}",
            "cwd": "${fileDirname}",
            "port": 9000
        }
    ]
}

我尝试开启编辑器中的日志,新增 log 参数为 true。结果如下:

// 开启时
<- launchResponse
Response {
  seq: 0,
  type: 'response',
  request_seq: 2,
  command: 'launch',
  success: true }
// 终止时
-> disconnectRequest
{ command: 'disconnect',
  arguments: { restart: false },
  type: 'request',
  seq: 3 }
<- disconnectResponse
Response {
  seq: 0,
  type: 'response',
  request_seq: 3,
  command: 'disconnect',
  success: true }

然而我尝试在服务端开启日志却找到不到日志文件。

我一直以为是我配置的原因,然而我在同事的机器上却成功断点了。

说明:我的环境为 macOS,同事是 Windows 10,我不知道这个问题是否和系统有关系。

调试了许久仍然没有结果,为此我在 DNMP 项目中发起了 issues,希望有人遇到过这个问题。经过漫长的等待,终于有朋友遇到过同样的问题并找到了解决办法。他原话如下:

@netwu: 9000端口已经被 php-fpm用了,所以得修改xdebug默认的端口,我用的9001...

我从未想过端口占用的问题,因为在 Windows 中我同事并未修改 XDebug 或者 php-fpm 的端口,这也是我不确定是否是因为操作系统造成的原因。

另外,同事的 XDebug 服务端的配置文件有做过修改,增加了 xdebug.remote_host 参数设置为172.25.160.1172.25.160.1 的来源是通过 ipconfig 命令查看到的 Docker 与宿主机通信的地址。

在 macOS 上这个地址通常使用 docker.for.mac.localhost 来代替。

最终我本地的配置如下:

// conf/php.ini
...
[XDebug]
xdebug.remote_enable = 1
#xdebug.remote_mode = "req"
#xdebug.remote_connect_back = on
xdebug.remote_autostart = on
xdebug.remote_host = docker.for.mac.localhost
xdebug.remote_port = 9001
#xdebug.remote_log = /var/log/php-fpm/x-debug-remote.log
...
// launch.json
{
    "version": "0.2.0",
    "configurations": [

        {
            "name": "Listen for XDebug",
            "type": "php",
            "request": "launch",
            "pathMappings": {
                "/var/www/html/project_name": "${workspaceRoot}"
            },
            "port": 9001
        },
        {
            "name": "Launch currently open script",
            "type": "php",
            "request": "launch",
            "program": "${file}",
            "cwd": "${fileDirname}",
            "port": 9001
        }
    ]
}

最后修改完配置文件记得重启 PHP 容器。

XDebug 的编译安装可以参考往期的一篇文章:PHP 安装 XDebug 并配置远程调试

上回的 Laravel 应用开发完成上线之后,稳定的跑了一个月。业务一切正常,就这最近一周应用负载的第二台服务器总是抽风。

先说说应用的场景。

我们的应用项目代码在我们自己的服务器上,两台服务器做承载。按理来说其中一台服务器宕掉会立马故障转移到另一台服务器。但是应用前边还有两台服务器。

不太好解释,一个请求差不多是这样的:

https://www.a.com/
       | 
       | (SLB 轮询到 Server 3、Server 4 上的一台服务器,然后 301 重定向)
       ↓
https://www.b.com/abc
       |
       |(SLB 轮询)
       ↓
Server 1、Server 2(所属其他项目组)
       |
       |(Nginx 转发)
       ↓
Server 3、Server 4 (所属我们项目组)
       |
       | (Nginx 转发到 Localhost 8800端口)
       ↓
      响应

这种场景有些特殊,困于公司的各种恼人的流程,实在想不出其他法子了,以至于开发的应用适配这个架构改了好多次。

现在问题是:当 Server 3Server 4 宕机了,SLB 无法故障转移。碰巧的是 php-fpmServer 4三天挂掉了三次

我们唯一知道服务器宕掉的途径是——业务群炸了!这显然已经晚了,业务势必受到影响。

在翻看 php-fpm 日志和 nginx 日志查找了半天没有结果的情况下,心生一秒计——Supervisor 守护进程。

Supervisor 简单来说就是在你需要常驻运行的程序挂掉的时候及时拉起。这对于现在的场景是非常合适啊。

编辑 /etc/supervisord.d/php-fpm.ini,配置如下:

[program:php-fpm]
command=bash -c "sleep 1 && /usr/local/php7/sbin/php-fpm --fpm-config /usr/local/php7/etc/php-fpm.conf --pid /usr/local/php7/var/run/php-fpm.pid"
process_name=%(program_name)s
autostart=true
autorestart=true
startretries=5
exitcodes=0,2,70
stopsignal=QUIT
stopwaitsecs=2
stdout_logfile=/data/logs/supervisord/php-fpm.log

进入 Supervisor 控制台:

$ sudo supervisorctl
nginx                            RUNNING   pid 26046, uptime 0:00:01
php-fpm                          STARTING
supervisor> reread
php-fpm: changed
supervisor> start php-fpm
php-fpm: ERROR (abnormal termination)

请记住这里最后的结果是 ERROR,然而查看进程 php-fpm 确实在跑了,kill 掉进程也能拉起来,我不太清楚为什么会这样。但是跑了一天后,php-fpm 又挂了。对,是又挂了而且没拉起来。

继续锁定 ERROR,先查看 php-fpm 日志。

[01-Sep-2018 11:20:21] ERROR: unable to bind listening socket for address '127.0.0.1:9000': Address already in use (98)
[01-Sep-2018 11:20:21] ERROR: FPM initialization failed
[01-Sep-2018 11:20:23] ERROR: unable to bind listening socket for address '127.0.0.1:9000': Address already in use (98)
[01-Sep-2018 11:20:23] ERROR: FPM initialization failed
[01-Sep-2018 11:20:24] ERROR: unable to bind listening socket for address '127.0.0.1:9000': Address already in use (98)
[01-Sep-2018 11:20:24] ERROR: FPM initialization failed
[01-Sep-2018 11:20:25] ERROR: unable to bind listening socket for address '127.0.0.1:9000': Address already in use (98)
[01-Sep-2018 11:20:25] ERROR: FPM initialization failed

一直在报这个错误,看来必须要解决才行呢,看样子是端口被占用了?但是是基于 supervisor 启动的,怎么会有这种错误呢?

当然,配置是有问题的。

php-fpm 进程默认是以 daemon 方式启动的,而 Supervisor 文档 的说明是,使用 supervisor 监护进程时,被监护的进程不能是守护进程。

我们需要关闭 php-fpm 的进程守护,编辑 /usr/local/php/etc/php-fpm.conf,查找 daemonize 修改为 no

然后 killall php-fpm 的所有进程,现在查看 php-fpm 日志。

$ tail -f /usr/local/php/var/log/php-fpm.log
[01-Sep-2018 11:28:25] ERROR: unable to bind listening socket for address '127.0.0.1:9000': Address already in use (98)
[01-Sep-2018 11:28:25] ERROR: FPM initialization failed
[01-Sep-2018 11:28:26] ERROR: unable to bind listening socket for address '127.0.0.1:9000': Address already in use (98)
[01-Sep-2018 11:28:26] ERROR: FPM initialization failed
[01-Sep-2018 11:28:27] ERROR: unable to bind listening socket for address '127.0.0.1:9000': Address already in use (98)
[01-Sep-2018 11:28:27] ERROR: FPM initialization failed
[01-Sep-2018 11:28:28] NOTICE: Terminating ...
[01-Sep-2018 11:28:28] NOTICE: exiting, bye-bye!
[01-Sep-2018 11:28:29] NOTICE: fpm is running, pid 26011
[01-Sep-2018 11:28:29] NOTICE: ready to handle connections

查看 supervisor 状态:

$ sudo supervisorctl
nginx                            RUNNING   pid 26046, uptime 0:00:01
php-fpm                          RUNNING   pid 26009, uptime 0:00:45

关于 php-fpm 为什么会隔三差五挂掉还没查出来,为了不影响业务,只能先守护进程保证业务正常运作。

起因

周五在公司加班的时候,调试了一个用户验证短信的一个 Bug,为了尽量做到接口的调用限制,对发送过的手机号设置了一定时长的「黑名单」机制,原理是将发送过的手机号存入缓存中,发送短信的时候获取缓存是否存在,如果存在就说明超频发送了。说实话很简单的一个功能,我却破天荒的调试了半天没搞明白。

后来,老大就坐在旁边,我俩一起调,从生成验证码到发送日志,全给丢到 debug 日志,挂着 tail 命令监听。

然鹅!模拟用户注册居然没有打印验证码日志,只有发送日志!

没记录日志,还始终报验证码错误,真是莫名其妙。

调试

首先,我将视线转移到缓存时间上,发现缓存时间只有 1 分钟,也就是说如果用户填写资料比较慢的话,验证码很可能就过期了!调试嘛,改到 10 分钟先。提交后更新生产环境代码。还是报错!

emmm…难道缓存系统出问题了?还是说 Redis 写不进去了?发送完验证码后,使用 Laravelartisan tinker 打印缓存,居然不是验证码,是手机号!诶,我这儿不是改了吗?(没错,刚开始的时候我勿将手机号存到 values 去了)。代码里加日志,想打印下看下什么情况。可是新加的打印日志死活打印不出来。

不行了,我要冷静下,慢慢的开始分析修改前和修改后的逻辑。

修改前:用户传入手机号->系统生成验证码(这里验证码是手机号)->将发送任务丢到队列->用户收到短信后填写->后端校验。

修改后:用户传入手机号->系统生成验证码(这里新加了日志输出)->将发送任务丢到队列->用户收到短信后填写->后端校验。

而现在的问题是:系统生成的验证码还是手机号,尽管我已经修改了;调试日志不打印!

也就是说。。队列没更新!

解决

分析出队列没更新,我立马就去查了 supervisor 的文档。之前更新队列都只使用了 rereadupdate 命令,只是更新了配置文件,但是队列并没有重新启!而 Laravel 的文档 队列 中提到:

注意一旦 queue:work 命令开始执行,它会一直运行直到它被手动停止或终端被关闭。

supervisor 是不会被关闭的,何况我给了 4 个子进程。而文档中又提到:

记住,队列处理器是一个常驻的进程并且在内存中保存着已经启动的应用状态。因此,它们并不会在启动后注意到你代码的更改。所以,在你的重新部署过程中,请记得 重启你的队列处理器.

也就是说,队列还是保持第一次执行的状态。难怪缓存值修改了没用,日志打印不出来也是这个原因。

重启 supervisor 队列命令:

supervisorctl restart laravel-queue

问题得到解决,真是被自己写的 Bug 蠢哭了。