需求

在vue单页应用开发中,如果为了省事PHP代码和vue代码都放在同一个GIT仓库中
那么如何进行vue前端代码和php Api 后端代码组织呢?
下面提供一个组织方式

项目目录结构

frontend
public
.gitignore
.travis.yml
application
build.php
composer.json
extend
LICENSE.txt
phpunit.xml
README.md
runtime
tests
think
thinkphp
vendor

其中
frontend 为vue前端项目目录
public 为php的web目录,index.php入口文件在这里,nginx也配置的是public

vue 配置

文件 frontend/config/index.js

module.exports = {
  build: {
    env: require('./prod.env'),
    index: path.resolve(__dirname, '../../public/ui/index.html'),
    assetsRoot: path.resolve(__dirname, '../../public/ui'),
    assetsSubDirectory: 'static',
    assetsPublicPath: '/ui/',
    .........其他代码.......
  },

前台访问

api接口为
http://appweb.dev/v1/api

访问/ui/即可
http://appweb.dev/ui/

需要注意的问题

public/ui 目录为前端项目build后的文件,不适宜纳入git管理
线上发布的通过CI&CD工具解决
目前适用于PHP项目的CI&CD工具有
Jenkins
Walle 瓦力上线部署系统
Piplin 持续集成与部署系统

未完待续

img

服务注册发现(Service Discovery)

  • CoreDNS
    https://coredns.io
  • Consul
    https://www.hashicorp.com/products/consul
  • Etcd
    https://github.com/coreos/etcd
  • Zookeeper
    https://zookeeper.apache.org
  • Eureka
    https://github.com/Netflix/eureka

负载均衡(Load Balancer)

  • Nginx
  • Haproxy

网关(API Gateway)

  • Kong

熔断(Circuit breaker)

  • Hystrix

分布式跟踪(Distributed Tracing)

  • Opentracing
    http://opentracing.io
    Opentracing是一个调用链路追踪规范,它类似于Zipkin和Google Dapper。
    与Zipkin不同的是,它定义了协议,并提供了多种语言的客户端库,但是没有提供最终存储和展示的实现。 用户可以自定义对接到不同的后端兼容层上,只要其兼容于Opentracing协议即可。
  • Jaeger
    https://github.com/jaegertracing/jaeger

参考资料

OpenTracing官方标准-中文版 https://github.com/opentracing-contrib/opentracing-specification-zh
Opentracing http://dmdgeeker.com/goBook/docs/ch11/opentracing.html
Uber Jaeger安装与使用 http://dmdgeeker.com/post/uber_jaeger/

监控(Monitoring)

  • Prometheus
    https://prometheus.io/

参考资料

Grafana https://grafana.com 监控展示面板

配置

  • confd

扩展资料

其他资料

CNCF(Cloud Native Computing Foundation)于 2015 年 7 月成立,隶属于 Linux 基金会,初衷围绕“云原生”服务云计算,致力于维护和集成开源技术,支持编排容器化微服务架构应用。

Projects


还不了解 CNCF?关于 CNCF 的三问三答! http://blog.daocloud.io/cncf-3/
Go使用grpc+http打造高性能微服务 https://mp.weixin.qq.com/s?__biz=MjM5OTcxMzE0MQ==&mid=2653370431&idx=1&sn=59175120599a0974eb32364c26421c09&chksm=bce4d8258b935133c80658431cde2dd5cfecb2cc6bd450d305e8242dc37288b9c0a9956e8423&mpshare=1&scene=1&srcid=1122W2gv4zaFKq6DaTg8jA0C#rd
关于负载均衡和服务发现,Google的经验在这里 http://blog.shurenyun.com/untitled-95/
12-Factor 软件设计12要素中文版 https://12factor.net/zh_cn/
Spring Cloud for Microservices Compared to Kubernetes

一些有用的综合Docker用法

crontab计划任务里面执行 docker中的程序,并指定账户

*/1 * * * * docker exec –user=www php-5.6-fpm-alpine bash -c “cd /tmp && pwd && ls && php -v > phpversion.log”

运行一个docker 命名且挂载目录映射端口

docker run -d –name php-5.6-fpm-alpine –restart=always -p 127.0.0.1:9001:9000 -v /tmp:/tmp -v /data:/data registry.cn-shanghai.aliyuncs.com/wwek/php:5.6-fpm-alpine

Docker 镜像加速

  • Docker 中国官方镜像加速 https://www.docker-cn.com/registry-mirror
    您可以配置 Docker 守护进程默认使用 Docker 官方镜像加速。这样您可以默认通过官方镜像加速拉取镜像,而无需在每次拉取时指定 registry.docker-cn.com。
    您可以在 Docker 守护进程启动时传入 –registry-mirror 参数:
$ docker --registry-mirror=https://registry.docker-cn.com daemon

为了永久性保留更改,您可以修改 /etc/docker/daemon.json 文件并添加上 registry-mirrors 键值。

{
  "registry-mirrors": ["https://registry.docker-cn.com"]
}

修改保存后重启 Docker 以使配置生效。

  • 阿里云 镜像加速器
    登录阿里云控制台 > 容器镜像服务 > 镜像加速器 按照后台给出的操作文档进行设置

免费的Docker私有镜像仓库

  • 阿里云
    登录阿里云控制台 > 容器镜像服务
    先设置”命名空间”,再在”镜像列表”,”创建镜像仓库”
    阿里云提供 阿里云Code,GitHub Bitbucket 私有GitLab 本地仓库 的源代码仓库的集成

  • 腾讯云
    登录腾讯云控制台 > 容器服务 > 镜像仓库
    先在”我的镜像”设置”命名空间”,再在”我的镜像”,”新建”
    腾讯云提供 Github GitLab 的源代码仓库的集成

管理工具

  • scope
    https://github.com/weaveworks/scope
    Monitoring, visualisation & management for Docker & Kubernetes
    可视化监控管理 Docker

  • kitematic
    Docker 官方桌面管理工具

mysql雪崩

故事

小王刚下班回家正准备露一手做2个菜。
手机就收到监控报警,网站打不开报警,mysql IO报警,xx报警
还没等开电脑,公司的人就来电话了
老板:小王啊,网站怎么又打不开了?快点解决啊!损失很大啊!
小王:好的马上处理。
凭借小王丰富的经验,mysql IO都那么高了,肯定是什么sql查询把IO都耗干净了,估计还卡了一堆查询
熟练的用show processlist看了下mysql正在处理的查询,果不其然,这查询队列都长到姥姥家了
苦逼的小心易易的结束了卡的时间很长的SELECT查询,这他妈结束的数据远远跟不上新增加的慢查询的速度呀
应用层程序也来不及改呀
最后小王还是恢复了数据库,可时间已经过去了整整1小时。

事后
老版:这次网站挂了1小时,以后别这样,的解决啊!
小王解释道:网站被恶意爬虫了,爬到了耗IO的sql查询,这个时候业务又是高峰期,导致数据库卡死了。
(对于业务来说,更多时候只关注结果,业务不能离线)

其他公司怎么做的?

  • 数据库中间件处理
  • 缓存机制保证不会让数据库太难受
  • 应用层熔断机制
    等等

纯PHP + MYSQL的项目如何应对数据库雪崩?

  • 数据库中间件处理
    我想绝大用PHP + MYSQL的中小公司是没有MYSQL中间件的,有中间件的可以在数据库中间件处理
    有用Swoole写MYSQL中间件的案例
  • 缓存要顶用
    设计好Redis机制,最小量的请求落在MYSQL上
  • 用MySQL Query Rewrite Plugin 进行SQL语句重写
    MySQL · 5.7新特性 · Query Rewrite Plugin
  • 用pt-kill自动杀掉过长的慢查询
    pt-kill是Percona-Toolkit系列之一,Percona公司出品的工具都没用过,绝对有相见恨晚的感觉。
    pt-kill杀掉慢查询,这算是一个简单粗暴的“熔断方案”,都已经到了让数据库雪崩的阶段,与其业务都不可用,还不如有损服务。
    需要注意的是只结束从库的SELECT慢查询

直接上手,pt-kill常驻进程自动结束SQL慢查询实例

以下实例作用是
* 127.0.0.1这台msyql实例上
* 字符集指定为 utf8
* 查询中有关键字 “SELECT|select” (支持正则)
* mysql帐号是 “业务mysql帐号”
* 查询时间大于60秒
* kill掉查询,不kill mysql连接
* 表示从匹配的结果中选择,类似SQL中的where部分,all是全部的查询
* 10秒检查一次
* 日志记录位置
* 打印日志
* 守护进程的方式运行

pt-kill \
--host 127.0.0.1 --port 3306 --user 'root' --password 'password' \
--charset utf8 \
--match-info "SELECT|select" \
--match-user 业务mysql帐号 \
--busy-time 60 \
--kill-query \
--victims all \
--interval 10 \
--log=/var/log/mysql-pt-kill.log \
--print \
--daemonize

常用参数说明

  • host 连接数据库的地址
  • port 连接数据库的端口
  • user 连接数据库的用户名
  • passowrd 连接数据库的密码
  • charset 指定字符集
  • match-command 指定杀死的查询类型
  • match-user 指定杀死的用户名,即杀死该用户的查询
  • match-info 匹配查询的信息
  • busy-time 指定杀死超过多少秒的查询
  • kill 执行kill命令
  • kill-query 与kill匹配查询的连接不同,此选项只会kill查询,而不会杀死连接
  • victims 表示从匹配的结果中选择,类似SQL中的where部分,all是全部的查询
  • interal 每隔多少秒检查一次
  • print 把kill的查询打印出来
  • log 输出日志到文件仅在 daemonize 守护进程的时候
  • daemonize 守护进程(常驻进程)
    更多参考官方pt-kill手册

监控多个mysql

如果多个mysql都需要用pt-kill监控慢查询并干掉,那么运行多个即可

/usr/bin/pt-kill \
--host 10.10.0.1 \
--port 3306 \
--user ptkill \ 
--password ptkill账号密码 \
--charset utf8 \
--match-info 'SELECT|select' \ 
--match-user 业务mysql帐号 \
--busy-time 20 \
--victims all \
--interval 10 \
--log=/var/log/pt-kill-mysql-slave1.log \
--pid=/var/run/pt-kill-mysql-slave1.pid \
--kill \
--print \ 
--daemonize

/usr/bin/pt-kill \
--host 10.10.0.2 \
--port 3306 \
--user ptkill \ 
--password ptkill账号密码 \
--charset utf8 \
--match-info 'SELECT|select' \ 
--match-user 业务mysql帐号 \
--busy-time 20 \
--victims all \
--interval 10 \
--log=/var/log/pt-kill-mysql-slave2.log \
--pid=/var/run/pt-kill-mysql-slave2.pid \
--kill \
--print \ 
--daemonize

pt-kill守护进程

虽然pt-kill自带参数daemonize可以后台守护运行,但是很多时候会莫名其妙的进程挂掉,
那么还需要一个外部的进程守护工具来守护他,在pt-kill自动退出后再次把进程拉起来
这里进程守护工具我们使用 Monit
配置文件参考为

#pt-kill-mysql 从库1守护
check process pt-kill-mysql-slave1 with pidfile /var/run/pt-kill-mysql-slave1.pid
   start program  "/usr/bin/pt-kill --host 10.10.0.1 --port 3306 --user ptkill --password ptkill账号密码 --charset utf8 --match-info 'SELECT|select' --match-user 业务mysql帐号 --busy-time 20 --victims all --interval 10 --log=/var/log/pt-kill-mysql-slave1.log --kill --print --pid=/var/run/pt-kill-mysql-slave1.pid --daemonize"
   stop program  "kill `cat /var/run/pt-kill-mysql-slave1.pid`"
   if changed pid then start

#pt-kill-mysql 从库2守护
check process pt-kill-mysql-slave2 with pidfile /var/run/pt-kill-mysql-slave2.pid
   start program "/usr/bin/pt-kill --host 10.10.0.2 --port 3306 --user ptkill --password ptkill账号密码 --charset utf8 --match-info 'SELECT|select' --match-user 业务mysql帐号 --busy-time 20 --victims all --interval 10 --log=/var/log/pt-kill-mysql-slave2.log --kill --print --pid=/var/run/pt-kill-mysql-slave2.pid --daemonize"
   stop program  "kill `cat /var/run/pt-kill-mysql-slave2.pid`"
   if changed pid then start

参考

官方pt-kill手册

需求来源,需要用PHP解析HTML提取我想要的数据

用PHP写网站爬虫的时候,需要把爬取的网页进行解析,提取里面想要的数据,这个过程叫做网页HTML中数据结构化。
很多人应该知道用phpQuery像JQuery一样的语法进行网页处理,抽取想要的数据。
但是在复杂一些的场景phpQuery并不能很好的完成工作,说简单点就是复杂场景不好用。
有没有更好的方式呢,我们看看商业爬虫软件是怎么做的。

他山之石,商业爬虫怎么做的

市面上商业爬虫怎么做HTML解析抽取数据的

看看市面上商业爬虫怎么做HTML解析结构化数据字段抽取
通过观察市面上商业爬虫工具GooSeeker、神箭手、八爪鱼,可以知道他们都用一个叫做XPath表达式的方式提取需要的字段数据。
那么可以判断使用XPath对HTML解析进行数据定位提取就是通行最佳的方式之一。

PHP中的XPath支持

XPath表达式可以查找HTML节点或元素,是一种路径表达语言。
那么需要先学习下XPath的基础,花个1-2小时入门,XPath就是页面数据提取能力的最佳内功之一,这个时间值得花。
既然用XPath提取页面数据是通行的方式,那么PHP中支持XPath的扩展包是什么呢?
为了帮大家节约时间,Symfony DomCrawler 就是PHP中最佳XPath包之一,直接用他吧,Symfony出品质量可是有目共睹,PHP热门框架laravel都用Symfony的包。
Symfony DomCrawler官方文档介绍的实例有限,建议有需要的情况下把代码读下,更能熟知新用法。

撸起袖子干,用DomCrawler做XPath HTML页面解析结构化数据抽取

基本思路

在Chrome浏览器中安装”XPath Helper”插件(XPath Helper怎么使用见参考资料)
打开需要解析的网站页面编写和测试XPath表达式
在PHP代码用DomCrawler使用上XPath表达式抽取想要的字段数据

实例演示

解析《神偷奶爸3》页面的的电影介绍信息
在Chrome中用编写测试XPath表达式

xpath helper使用


在项目下用composer安装guzzlehttp/guzzle(http client)、symfony/dom-crawler(Symfony DomCrawler)

composer require  guzzlehttp/guzzle
composer require  symfony/dom-crawler

下面直接上代码,在代码中用注释做说明
php Douban.php

<?php
/**
 * Created by PhpStorm.
 * User: wwek
 * Date: 2017/7/9
 * Time: 21:41
 */

require __DIR__ . '/vendor/autoload.php';

use GuzzleHttp\Client;
use Symfony\Component\DomCrawler\Crawler;

print_r(json_encode(Spider(), JSON_UNESCAPED_UNICODE));
//print_r(Spider());

function Spider()
{
    //需要爬取的页面
    $url = 'https://movie.douban.com/subject/25812712/?from=showing';

    //下载网页内容
    $client   = new Client([
        'timeout' => 10,
        'headers' => ['User-Agent' => 'Mozilla/5.0 (compatible; Baiduspider-render/2.0; +http://www.baidu.com/search/spider.html)',
        ],
    ]);
    $response = $client->request('GET', $url)->getBody()->getContents();

    //进行XPath页面数据抽取
    $data    = []; //结构化数据存本数组
    $crawler = new Crawler();
    $crawler->addHtmlContent($response);

    try {
        //电影名称
        //网页结构中用css选择器用id的比较容易写xpath表达式
        $data['name'] = $crawler->filterXPath('//*[@id="content"]/h1/span[1]')->text();
        //电影海报
        $data['cover'] = $crawler->filterXPath('//*[@id="mainpic"]/a/img/@src')->text();
        //导演
        $data['director'] = $crawler->filterXPath('//*[@id="info"]/span[1]/span[2]')->text();
        //多个导演处理成数组
        $data['director'] = explode('/', $data['director']);
        //过滤前后空格
        $data['director'] = array_map('trim', $data['director']);

        //编剧
        $data['cover'] = $crawler->filterXPath('//*[@id="info"]/span[2]/span[2]/a')->text();
        //主演
        $data['mactor'] = $crawler->filterXPath('//*[@id="info"]/span[contains(@class,"actor")]/span[contains(@class,"attrs")]')->text();
        //多个主演处理成数组
        $data['mactor'] = explode('/', $data['mactor']);
        //过滤前后空格
        $data['mactor'] = array_map('trim', $data['mactor']);

        //上映日期
        $data['rdate'] = $crawler->filterXPath('//*[@id="info"]')->text();
        //使用正则进行抽取
        preg_match_all("/(\d{4})-(\d{2})-(\d{2})\(.*?\)/", $data['rdate'], $rdate); //2017-07-07(中国大陆) / 2017-06-14(安锡动画电影节) / 2017-06-30(美国)
        $data['rdate'] = $rdate[0];
        //简介
        //演示使用class选择器的方式
        $data['introduction'] = trim($crawler->filterXPath('//div[contains(@class,"indent")]/span')->text());

        //演员
        //本xpath表达式会得到多个对象结果,用each方法进行遍历
        //each是传入的参数是一个闭包,在闭包中使用外部的变量使用use方法,并使用变量指针
        $crawler->filterXPath('//ul[contains(@class,"celebrities-list from-subject")]/li')->each(function (Crawler $node, $i) use (&$data) {
            $actor['name']   = $node->filterXPath('//div[contains(@class,"info")]/span[contains(@class,"name")]/a')->text(); //名字
            $actor['role']   = $node->filterXPath('//div[contains(@class,"info")]/span[contains(@class,"role")]')->text(); //角色
            $actor['avatar'] = $node->filterXPath('//a/div[contains(@class,"avatar")]/@style')->text(); //头像
            //background-image: url(https://img3.doubanio.com/img/celebrity/medium/5253.jpg) 正则抽取头像图片
            preg_match_all("/((https|http|ftp|rtsp|mms)?:\/\/)[^\s]+\.(jpg|jpeg|gif|png)/", $actor['avatar'], $avatar);
            $actor['avatar'] = $avatar[0][0];
            //print_r($actor);
            $data['actor'][] = $actor;
        });

    } catch (\Exception $e) {

    }

    return $data;

}

执行结果

{
    "name": "神偷奶爸3 Despicable Me 3",
    "cover": "https://img3.doubanio.com/view/movie_poster_cover/lpst/public/p2469070974.webp",
    "director": [
        "凯尔·巴尔达",
        "皮艾尔·柯芬"
    ],
    "mactor": [
        "史蒂夫·卡瑞尔",
        "克里斯汀·韦格",
        "崔·帕克",
        "米兰达·卡斯格拉夫",
        "拉塞尔·布兰德",
        "迈克尔·贝亚蒂",
        "达纳·盖尔",
        "皮艾尔·柯芬",
        "安迪·尼曼"
    ],
    "rdate": [
        "2017-07-07(中国大陆)",
        "2017-06-14(安锡动画电影节)",
        "2017-06-30(美国)"
    ],
    "introduction": "  《神偷奶爸3》将延续前两部的温馨、搞笑风格,聚焦格鲁和露西的婚后生活,继续讲述格鲁和三个女儿的爆笑故事。“恶棍”奶爸格鲁将会如何对付大反派巴萨扎·布莱德,调皮可爱的小黄人们又会如何耍贱卖萌,无疑让全球观众万分期待。该片配音也最大程度沿用前作阵容,史蒂夫·卡瑞尔继续为男主角格鲁配音,皮埃尔·柯芬也将继续为经典角色小黄人配音,而新角色巴萨扎·布莱德则由《南方公园》主创元老崔·帕克为其配音。",
    "actor": [
        {
            "name": "皮艾尔·柯芬 ",
            "role": "导演",
            "avatar": "https://img3.doubanio.com/img/celebrity/medium/1389806916.36.jpg"
        },
        {
            "name": "凯尔·巴尔达 ",
            "role": "导演",
            "avatar": "https://img3.doubanio.com/img/celebrity/medium/51602.jpg"
        },
        {
            "name": "史蒂夫·卡瑞尔 ",
            "role": "饰 Gru / Dru",
            "avatar": "https://img3.doubanio.com/img/celebrity/medium/15731.jpg"
        },
        {
            "name": "克里斯汀·韦格 ",
            "role": "饰 Lucy Wilde",
            "avatar": "https://img3.doubanio.com/img/celebrity/medium/24543.jpg"
        },
        {
            "name": "崔·帕克 ",
            "role": "饰 Balthazar Bratt",
            "avatar": "https://img3.doubanio.com/img/celebrity/medium/5253.jpg"
        },
        {
            "name": "米兰达·卡斯格拉夫 ",
            "role": "饰 Margo",
            "avatar": "https://img1.doubanio.com/img/celebrity/medium/1410165824.37.jpg"
        }
    ]
}

经验

网页上数据是动态变化的,没获取到的字段需要用try catch进行异常处理,这样程序就不会崩溃
XPath表达式孰能生巧基本能应付绝大多数的需求
某些数据如果用XPath表达式也不好取,或者取出来的数据还需要加工的,用正则表达式处理,用preg_match_all进行抽取,用preg_replace进行替换
用strip_tags()函数去除HTML、XML以及PHP的标签,加参数可以保留标签去除,如处理文章内容strip_tags($str, "<p><img><strong>"),后留后面参数中的标签
一些常用的正则表达式


$str=preg_replace("/\s+/", " ", $str); //过滤多余回车 $str=preg_replace("/<[ ]+/si","<",$str); //过滤<__("<"号后面带空格) $str=preg_replace("/<\!--.*?-->/si","",$str); //注释 $str=preg_replace("/<(\!.*?)>/si","",$str); //过滤DOCTYPE $str=preg_replace("/<(\/?html.*?)>/si","",$str); //过滤html标签 $str=preg_replace("/<(\/?head.*?)>/si","",$str); //过滤head标签 $str=preg_replace("/<(\/?meta.*?)>/si","",$str); //过滤meta标签 $str=preg_replace("/<(\/?body.*?)>/si","",$str); //过滤body标签 $str=preg_replace("/<(\/?link.*?)>/si","",$str); //过滤link标签 $str=preg_replace("/<(\/?form.*?)>/si","",$str); //过滤form标签 $str=preg_replace("/cookie/si","COOKIE",$str); //过滤COOKIE标签 $str=preg_replace("/<(applet.*?)>(.*?)<(\/applet.*?)>/si","",$str); //过滤applet标签 $str=preg_replace("/<(\/?applet.*?)>/si","",$str); //过滤applet标签 $str=preg_replace("/<(style.*?)>(.*?)<(\/style.*?)>/si","",$str); //过滤style标签 $str=preg_replace("/<(\/?style.*?)>/si","",$str); //过滤style标签 $str=preg_replace("/<(title.*?)>(.*?)<(\/title.*?)>/si","",$str); //过滤title标签 $str=preg_replace("/<(\/?title.*?)>/si","",$str); //过滤title标签 $str=preg_replace("/<(object.*?)>(.*?)<(\/object.*?)>/si","",$str); //过滤object标签 $str=preg_replace("/<(\/?objec.*?)>/si","",$str); //过滤object标签 $str=preg_replace("/<(noframes.*?)>(.*?)<(\/noframes.*?)>/si","",$str); //过滤noframes标签 $str=preg_replace("/<(\/?noframes.*?)>/si","",$str); //过滤noframes标签 $str=preg_replace("/<(i?frame.*?)>(.*?)<(\/i?frame.*?)>/si","",$str); //过滤frame标签 $str=preg_replace("/<(\/?i?frame.*?)>/si","",$str); //过滤frame标签 $str=preg_replace("/<(script.*?)>(.*?)<(\/script.*?)>/si","",$str); //过滤script标签 $str=preg_replace("/<(\/?script.*?)>/si","",$str); //过滤script标签 $str=preg_replace("/javascript/si","Javascript",$str); //过滤script标签 $str=preg_replace("/vbscript/si","Vbscript",$str); //过滤script标签 $str=preg_replace("/on([a-z]+)\s*=/si","On\\1=",$str); //过滤script标签 $str=preg_replace("/&#/si","&#",$str); //过滤script标签,如javAsCript:alert(

参考资料

官方文档The DomCrawler Component
八爪鱼的了解XPath常用术语和表达式解析 十分钟轻松入门
gooseeker的xpath基础知识培训
神箭手的常用的辅助开发工具
XPath Helper:chrome爬虫网页解析工具 Chrome插件图文教程
在PHP中,您如何解析和处理HTML/XML?

扩展阅读

关于反爬虫,看这一篇就够了
最好的语言PHP + 最好的前端测试框架Selenium = 最好的爬虫(上)
QueryList 简单、 灵活、强大的PHP采集工具,让采集更简单一点。