公司里使用dubbo做rpc,启动时dubbo的service会向zookeeper注册其自身,包含一些参数,如版本号,服务地址等等信息,但是zookeeper里的信息是urldecode的,非常不友好,又不想总去找个网站做urldecode,这时候脚本语言的优势就显现出来了

1
2
<?php
echo urldecode($argv[1]);

非常简单的命令,执行时直接把zookeeper里的内容复制下来
php xxx.php $url  就可以在cli里看到实际内容了

高性能PHP应用开发

读后感

几天时间,看完了这本书,书很薄,内容比较快餐,优化不应该是这么泛泛笼统的谈吧。
整本书介绍了很多工具,用工具做了一些分析,没什么实际经验分享。对于扩展视野还是有帮助,
比如:

  • 使用apache benchmark测试网站性能
  • 使用vld分析opcode
  • ngnix,apache等应用服务其的一些介绍
  • 使用yui压缩资源文件
    等等.

什么是docker

docker是一个开源项目,其目的是实现轻量级的操作系统虚拟化解决方案.
Docker 的基础是linux的容器(LXC)等技术


docker和vagrant的区别

stackoverflow上的一个问题,二楼是vagrant的作者,三楼是docker的作者


Docker的三个概念

  • 镜像(Image)
  • 容器(Container)
  • 仓库(Repository)
    理解这三个概念,就理解了Docker的整个生命周期

镜像

Docker镜像就是一个只读的模板。例如一个镜像可以包含一个完整的Ubuntu镜像,里面仅安装了某些你需要的应用程序,比如Apache+Mysql+php等等
镜像可以用来创建Docker容器
Docker提供了一个简单的方式来创建镜像或是更新现有镜像DockerHub(有点像github),你可以直接从官方或是其他人那里获得镜像直接使用

Docker 容器

Docker 利用容器来运行应用。
容器是镜像创建的运行实例,每个容器可以是隔离的,保证安全的平台。
镜像是只读的,容器在启动的时候创建一层可写层作为最上层。

Docker仓库

仓库是集中存放镜像文件的场所,有公有仓库和私有仓库
公有仓库就是我们前面说的DockerHub,用户可以在本地网络中创建一个私有仓库
用户可以创建自己的镜像并push到共有或私有仓库,方便下次或另外机器上使用
和github类似,不多说了


安装

Ubuntu系统安装(我的是13.10系统 这种安装方式不是最新的Docker)

1
2
3
4
$ sudo apt-get update
$ sudo apt-get install docker.io
$ sudo ln -sf /usr/bin/docker.io /usr/local/bin/docker
$ sudo sed -i '$acomplete -F _docker docker' /etc/bash_completion.d/docker.io


运行一个Docker实例

首先要注册一个Docker账户这里,邮箱确认后就可以登录了

1
$ sudo docker login

登录后,可以输入$ docker run来查看可用指令
接下来 试一试$ sudo docker run ubuntu:14.04 /bin/echo 'Hello world'
sudo docker run ubuntu:14.04告诉 Docker加载 ubuntu 14.04。
Docker在运行时会判断当前运行的镜像是否加载到Docker主机上,如果没有,会到Docker下载指定的镜像

1
Hello world

以上 就安装并运行了一个简单的Docker实例

范型方法的反模式

原文地址
英文地址

我承认,我自己也忍不住用过这项技术。它简直太方便了,可以省去一次不必要的类型转化。这就是:

1
2
3
4
interface SomeWrapper {
<T> T get();
}

现在你便可以安全地将包装类转换成任意类型了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
SomeWrapper wrapper = ...
// Obviously
Object a = wrapper.get();
// Well...
Number b = wrapper.get();
// Risky
String[][] c = wrapper.get();
// Unprobable
javax.persistence.SqlResultSetMapping d =
wrapper.get();
`

这也正是你在使用JOOR时所用到的API,jOOR是我们开发并开源的一个反射工具库,它可以提升我们编写集成测试的效率。有了jOOR,你可以这么编写代码:

1
2
3
4
5
6
7
8
9
10
11
Employee[] employees = on(department)
.call("getEmployees").get();
for (Employee employee : employees) {
Street street = on(employee)
.call("getAddress")
.call("getStreet")
.get();
System.out.println(street);
}

这个API非常简单。on()方法会对某个对象或类进行封装。call()方法会通过反射去调用这个对象上的方法(不需要签名正确,也不需要方法声明成public,同时也不会抛出任何的受检查异常)。同时你也不需要强制类型转换,便能通过get()方法将结果转换成任意的类型。

对于一款像jOOR这样的反射库而言,这么做是没问题的,因为这个库本身就不是类型安全的。当然不会安全了,因为本来就是反射。

不过这还是会让人感到有些不爽。感觉就是在调用点这里承诺会返回某个类型,但实际上却无法兑现,这很可能会导致ClassCastException异常——在有了Java 5以及泛型之后才开始写Java的开发人员是很难体会到这种感觉的

但JDK也是这么做的。。

没错,JDK是这么干了。不过这样的情况并不多,并且只是在泛型参数的类型并不那么重要的情况下才会这么做。比如说,当你通过Collection.emptyList()获取一个空列表的时候,这个方法的实现是这样的

1
2
3
4
5
@SuppressWarnings("unchecked")
public static final <T> List<T> emptyList() {
return (List<T>) EMPTY_LIST;
}

没错,EMPTY_LIST从List强转成List是挺不安全的。但从语义层面来说,这样的强转是安全的。你不能修改这个列表,因为这是个空列表。List中也不会有任何一个方法会返回给你一个非目标类型的T或者T[]的实例。因此,这些做法都是合法的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// perfectly fine
List<?> a = emptyList();
// yep
List<Object> b = emptyList();
// alright
List<Number> c = emptyList();
// no problem
List<String[][]> d = emptyList();
// if you must
List<javax.persistence.SqlResultSetMapping> e = emptyList();

JDK的开发人员从来都会非常小心地不去对你所可能获取到的泛型类型做出任何无法兑现的承诺。也就是说,即便存在一个更适合的类型的时候,通常返回给你的也都是Object类型。

哪个类型更适合只有你知道,编译器可不了解。类型擦除是有代价的,代价就是当包装类或者集合为空的时候。像这样的一个表达式编译器是无法得知它的类型的,它可不能不懂装懂。也就是说:

不要使用这种只为了省一次类型转化的泛型方法的反模式。

关于30种技术

这是segmentfault上的一个系列,做业务做累了就该找点东西玩玩,不是么?
但是说实话,这仅仅是了解,熟悉真谈不上,业务中没有真正使用到,都是纸上谈兵


第一天 Bower

原文地址:Day 1: Bower —— 管理你的客户端依赖关系
英文原文在这里
由于环境的千差万别,遇到的问题肯定会不一样,所以我在跟着原文使用时也会遇到一些问题,我会将我遇到的问题整理出来
Bower是一个客户端技术的软件包管理器,它可用于搜索、安装和卸载如JavaScript、HTML、CSS之类的网络资源。其他一些建立在Bower基础之上的开发工具,如YeoMan和Grunt,这个会跟着写 :)


为什么我会在意Bower?

  • 节省时间。为什么要学习Bower的第一个原因,就是它会为你节省寻找客户端的依赖关系的时间。每次我需要安装jQuery的时候,我都需要去jQuery网站下载包或使用CDN版本。但是有了Bower,你只需要输入一个命令,jquery就会安装在本地计算机上,你不需要去记版本号之类的东西,你也可以通过Bower的info命令去查看任意库的信息。
  • 脱机工作。Bower会在用户主目录下创建一个。bower的文件夹,这个文件夹会下载所有的资源、并安装一个软件包使它们可以离线使用。如果你熟悉Java,Bower即是一个类似于现在流行的Maven构建系统的。m2仓库。每次你下载任何资源库都将被安装在两个文件夹中一个在的应用程序文件夹,另一个在用户主目录下的。bower文件夹。因此,下一次你需要这个仓库时,就会用那个用户主目录下.bower中的版本。
  • 可以很容易地展现客户端的依赖关系。你可以创建一个名为bower。json的文件,在这个文件里你可以指定所有客户端的依赖关系,任何时候你需要弄清楚你正在使用哪些库,你可以参考这个文件。
  • 让升级变得简单。假设某个库的新版本发布了一个重要的安全修补程序,为了安装新版本,你只需要运行一个命令,bower会自动更新所有有关新版本的依赖关系。

前提准备

为了安装bower,你首先需要安装如下文件:

  • node.js + npm 安装步骤看这里
  • git 需要从github上下载代码包,github都不会用也不会找到我的博客 :)

安装bower

安装好准备的东西,就可以安装bower了
sudo npm install -g bower
-g 表示全局安装


运行bower

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
$ bower help
Usage:
bower <command> [<args>] [<options>]
Commands:
cache Manage bower cache
help Display help information about Bower
home Opens a package homepage into your favorite browser
info Info of a particular package
init Interactively create a bower.json file
install Install a package locally
link Symlink a package folder
list List local packages - and possible updates
lookup Look up a package URL by name
prune Removes local extraneous packages
register Register a package
search Search for a package by name
update Update a local package
uninstall Remove a local package
version Bump a package version
Options:
-f, --force Makes various commands more forceful
-j, --json Output consumable JSON
-l, --log-level What level of logs to report
-o, --offline Do not hit the network
-q, --quiet Only output important information
-s, --silent Do not output anything, besides errors
-V, --verbose Makes output more verbose
--allow-root Allows running commands as root
--version Output Bower version
See 'bower help <command>' for more information on a specific command.

运行bower help 时遇到了如下错误
/usr/bin/env: node: 没有那个文件或目录

修改/usr/local/bin/bower中的第一行,
#!/usr/bin/env node 改成#!/usr/bin/env nodejs 再启动就好了


包的安装

Bower是一个软件包管理器,所以你可以在应用程序中用它来安装新的软件包。举例来看一下来如何使用Bower安装JQuery,在你想要安装该包的地方创建一个新的文件夹,键入如下命令:
bower install jquery
由于没有权限 安装出现错误

1
2
3
4
5
6
7
8
9
10
11
$ bower install jquery
bower jquery#* not-cached git://github.com/jquery/jquery.git#*
bower jquery#* resolve git://github.com/jquery/jquery.git#*
bower jquery#* download https://github.com/jquery/jquery/archive/2.1.1.tar.gz
bower jquery#* extract archive.tar.gz
bower jquery#* resolved git://github.com/jquery/jquery.git#2.1.1
bower EACCES EACCES, mkdir '/usr/bin/bower_components'
Stack trace:
Error: EACCES, mkdir '/usr/bin/bower_components'
....

然后使用sudo = =#

1
2
3
4
5
6
7
8
9
10
11
12
$ sudo bower install jquery
[sudo] password for ericwang:
bower ESUDO Cannot be run with sudo
Additional error details:
Since bower is a user command, there is no need to execute it with superuser permissions.
If you're having permission errors when using bower without sudo, please spend a few minutes learning more about how your system should work and make any necessary repairs.
http://www.joyent.com/blog/installing-node-and-npm
https://gist.github.com/isaacs/579814
You can however run a command with sudo using --allow-root option

提示就不翻译了,使用如下命令 安装成功
$ bower --allow-root install jquery
引起这个的原因是因为bower是在当前文件夹下创建bower_components文件夹,而bower是没有root权限的(我是因为改动/usr/local/bin/bower进入到/usr/local/bin/下的,所以运行命令时就会遇到权限问题),所以在root权限文件夹下使用该命令就会导致这种问题,如果你一定要在该文件夹下创建,可以使用–allow-root参数,或者修改文件夹的权限


包的使用

现在就可以在应用程序中使用jQuery包了,在jQuery里创建一个简单的html5文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!doctype html>
<html>
<head>
<title>Learning Bower</title>
</head>
<body>
<button>Animate Me!!</button>
<div style="background:red;height:100px;width:100px;position:absolute;">
</div>
<script type="text/javascript" src="bower_components/jquery/jquery.min.js"></script>
<script type="text/javascript">
$(document).ready(function(){
$("button").click(function(){
$("div").animate({left:'250px'});
});
});
</script>
</body>
</html>

正如你所看到的,你刚刚引用jquery.min.js文件,现阶段完成。

其他命令

1
2
3
4
5
6
bower list //所有安装过的包的列表
search bootstrap //假如你想在你的应用程序中使用twitter的bootstrap框架,但你不确定包的名字,这时你可以使用search 命令
info bootstrap //如果你想看到关于特定的包的信息,可以使用info 命令来查看该包的所有信息 还可以这样
uninstall jquery //卸载包可以使用uninstall 命令
install //安装包 (bower install bootstrap#2.1.1 --save
安装指定版本,不过如果之前安装了某版本,会提示冲突,跟着提示走就可以了,bower.json也会被更新)

bower.json文件的使用

bower.json文件的使用可以让包的安装更容易,你可以在应用程序的根目录下创建一个名为“bower.json”的文件,并定义它的依赖关系。使用bower init 命令来创建bower.json文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
$ bower init
? name: bower-test
? version: 0.0.1
? description: Add bower.json
? main file:
? what types of modules does this package expose?:
? keywords:
? authors: ericwang
? license: MIT
? homepage:
? set currently installed components as dependencies?: Yes
? add commonly ignored files to ignore list?: Yes
? would you like to mark this package as private which prevents it from being accidentally published to the registry?: Yes
{
name: 'bower-test',
version: '0.0.1',
description: 'Add bower.json',
authors: [
'ericwang'
],
license: 'MIT',
private: true,
ignore: [
'**/.*',
'node_modules',
'bower_components',
'test',
'tests'
],
dependencies: {
jquery: '~2.1.1'
}
}
? Looks good?: Yes

可以查看该文件view bower.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
"name": "bower-test",
"version": "0.0.1",
"description": "Add bower.json",
"authors": [
"ericwang"
],
"license": "MIT",
"private": true,
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"test",
"tests"
],
"dependencies": {
"jquery": "~2.1.1"
}
}

已经加入了jQuery依赖关系
现在假设也想用twitter bootstrap,我们可以用下面的命令安装twitter bootstrap并更新bower.json文件:
$ bower install bootstrap --save
它会自动安装最新版本的bootstrap并更新bower.json文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
"name": "bower-test",
"version": "0.0.1",
"description": "Add bower.json",
"authors": [
"ericwang"
],
"license": "MIT",
"private": true,
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"test",
"tests"
],
"dependencies": {
"jquery": "~2.1.1",
"bootstrap": "~3.2.0"
}
}


玩完了

原谅我这一生不羁放纵爱自由,上班时间写博客,罪过罪过。
用过java的maven管理,理解起来这个就相对简单了。
搭建依赖库时,最讨厌的就是到处找依赖包不是么,而有了这些工具之后,都省掉了。

起因

这大概是很多人都想知道的吧,开发的时候,能看到程序真正执行的是什么可以减少很多时间去调试。
尤其是在当今IOC,MVC框架中,很多只懂得复制粘贴的程序员们遇到问题就只能问,根本不去深究为什么可以这样
废话真多,不说了,先来看这段代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?php
class PDO_TEST{
public function test($config,$id){
$pdo = new PDO($config['host'],$config['user'],$config['pass'],$config['init_statements']);
$sql = 'SELECT * FROM book WHERE `id` = :id';
$stmt = $pdo->prepare($sql);
$stmt->bindParam(':id',$id);
$stmt->execute();
return $stmt->fetchAll();
}
}
$config = array(
'host'=>'mysql:host=192.168.188.48;dbname=test',
'user' => 'admin',
'pass' => 'admin',
'init_statements' => array(
'SET CHARACTER SET utf8',
'SET NAMES utf8'
),
'default_fetch_mode' => PDO::FETCH_ASSOC
);
$id =1;
$test = new PDO_TEST();
$result =$test->test($config,$id);
var_dump($result);exit;
?>


tcpdump

如果直接var_dump($sql);返回的只是绑定的sql,即一个不完整的sql,还要去var_dump($id),才能完整地显示出来sql语句,而有时候,这种调试显得异常笨拙。
我们急需一种方法来快速定位并解决sql问题

1
sudo tcpdump -n -X -S -i eth0 port 3306 and host 192.168.188.48

使用man tcpdump可以查看tcpdump工具的具体使用

1
2
3
4
5
6
-n Don't convert addresses (i.e., host addresses, port numbers, etc.) to names. #不要对地址进行转换
-X When parsing and printing, in addition to printing the headers of each packet, print the data of each packet (minus its link level
header) in hex and ASCII. This is very handy for analysing new protocols.
#-x/-xx/-X/-XX:以十六进制显示包内容,几个选项只有细微的差别,详见man手册。
-S Print absolute, rather than relative, TCP sequence numbers. #显示绝对的Sequence Number
-i Listen on interface. #我本地的网卡是eth0

其他的就不多解释了,也不会,跟着干货师傅学到的~
这样就能在本地看到有一部分包中包含如下内容

1
2
3
4
5
6
0x0000: 4500 005d af32 4000 4006 8d5a c0a8 c08c E..].2@.@..Z....
0x0010: c0a8 bc30 e48e 0cea 7929 223b 4f09 64ca ...0....y)";O.d.
0x0020: 8018 00e5 fe5d 0000 0101 080a 005f 8ee6 .....]......._..
0x0030: 0069 7c2c 2500 0000 0353 454c 4543 5420 .i|,%....SELECT.
0x0040: 202a 2046 524f 4d20 626f 6f6b 2057 4845 .*.FROM.book.WHE
0x0050: 5245 2060 6964 6020 3d20 2731 27 RE.`id`.=.'1'

实际上能看到发送的包是因为mysql的预处理机制
这里有一些关于预处理的翻译
这里的note也不错
With PDO_MYSQL you need to remember about the PDO::ATTR_EMULATE_PREPARES option.

The default value is TRUE, like
$dbh->setAttribute(PDO::ATTR_EMULATE_PREPARES,true);

This means that no prepared statement is created with $dbh->prepare() call. With exec() call PDO replaces the placeholders with values itself and sends MySQL a generic query string.

The first consequence is that the call $dbh->prepare(‘garbage’);
reports no error. You will get an SQL error during the $dbh->exec() call.
The second one is the SQL injection risk in special cases, like using a placeholder for the table name.

The reason for emulation is a poor performance of MySQL with prepared statements. Emulation works significantly faster.

小结

如果添加如下改动,这种方式放弃了预编译

1
2
3
$pdo = new PDO($config['dsn'],$config['username'],$config['password'],$config['init_statements']);
+ $pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$sql = 'SELECT * FROM book WHERE `id` = :id';

两个包的接收片段

1
2
3
4
5
6
0x0000: 4500 005b a449 4000 4006 9845 c0a8 c08c E..[.I@.@..E....
0x0010: c0a8 bc30 e73c 0cea a759 8d95 4b69 5fc7 ...0.<...Y..Ki_.
0x0020: 8018 00e5 fe5b 0000 0101 080a 0064 2962 .....[.......d)b
0x0030: 006e 16a6 2300 0000 1653 454c 4543 5420 .n..#....SELECT.
0x0040: 202a 2046 524f 4d20 626f 6f6b 2057 4845 .*.FROM.book.WHE
0x0050: 5245 2060 6964 6020 3d20 3f RE.`id`.=.?

1
2
3
4
5
0x0000: 4500 0048 a44a 4000 4006 9857 c0a8 c08c E..H.J@.@..W....
0x0010: c0a8 bc30 e73c 0cea a759 8dbc 4b69 608e ...0.<...Y..Ki`.
0x0020: 8018 00ed fe48 0000 0101 080a 0064 2962 .....H.......d)b
0x0030: 006e 16a6 1000 0000 1701 0000 0000 0100 .n..............
0x0040: 0000 0001 fd00 0131 .......1

尽管exec方法和查询在PHP中仍然被大量使用和支持,但是PHP官网上还是要求大家用预处理语句的方式来替代。为什么呢?主要是因为:它更安全。预处理语句不会直接在实际查询中插入参数,这就避免了许多潜在的SQL注入。

然而出于某种原因,PDO实际上并没有真正的使用预处理,它是在模拟预处理方式,在将语句传给SQL服务器之前会把参数数据插入到语句中,这使得某些系统容易受到SQL注入。
实际上$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES,true); 使得pdo将语句进行了本地处理,并直接向mysql提交了sql语句。
而$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES,false);则是先向sql服务器发送prepare中的语句,接受到服务器返回的一些数据(meta data),再发送参数
这样做安全,但是速度变慢了

问题

今天在处理dbrt时,类似的建表语句如下:

1
2
3
4
5
CREATE TABLE `xxx`(
...
`column1` int(11) not null
...
)

对于这种字段,需要赋予默认值,然后有人说,我设置的不为null,就是不希望有空值插入进去,所以这里不需要设置default。


深入探究null 和 not null

首先,我们要搞清楚“空值” 和 “NULL” 的概念:

  • 空值是不占用空间的
  • mysql中的NULL其实是占用空间的,下面是来自于MYSQL官方的解释
1
NULL columns require additional space in the row to record whether their values are NULL. For MyISAM tables, each NULL column takes one bit extra, rounded up to the nearest byte

所以空值还是会被插入到db中,只有NULL列才不会被插入。

为什么要设置NOT NULL

NULL列需要更多的存储空间,还需要mysql内部进行特殊处理。NULL列被索引后,每条记录都需要一个额外的字节,还能导致MYisam 中固定大小的索引变成可变大小的索引。这在《高性能mysql》中有介绍的,
所以为了尽量避免NULL,我们指定列为NOT NULL。
可以看出,在MySQL中,含有NULL的列很难进行查询优化,因为它们使得索引、索引的统计信息以及比较运算更加复杂。你应该用0、一个特殊的值或者一个空串代替空值。

##问题
同事问了一个问题:
框架底层对executeSql进行了封装(对sql进行了处理,做主从分离),在执行insert xxx values xxx on duplicate key update语句后,使用lastInsertId时返回结果是0。

原因

由于要判断是否插入失败,所以分析了一下框架executeSql,除了select,desc 其他语句使用master进行处理(主要是为了实现框架级别的主从分离),在PDOStatement::execute之后,
对操作语句的类型进行了判断,对于insert的返回,执行成功的结果都是返回PDO::lastInsertId,如果执行失败,则返回false.
查看了PDO::lastInsertId 的说明手册

1
2
3
4
5
string PDO::lastInsertId ([ string $name = NULL ] )
返回最后插入行的ID,或者是一个序列对象最后的值,取决于底层的驱动。比如,PDO_PGSQL() 要求为 name 参数指定序列对象的名称。
Note:
在不同的 PDO 驱动之间,此方法可能不会返回一个有意义或一致的结果,因为底层数据库可能不支持自增字段或序列的概念。

但是我的数据库底层支持自增字段或序列啊,不应该返回0,结果仔细看表发现,由于表是从其他表中获取有效数据同步过来的,所以没有自增字段(auto-increment),
这样,判断executeSql是否执行失败,需要使用result === false来判断了

单例模式

经过良好设计的系统一般通过方法调用传递对象实例。通常单例使用与如下背景环境:
1.对象会被系统中的任何对象使用
2.对象不应该储存在会被修改的全局变量中
3.系统中不应该超过一个该对象,也就是说Y设置了X对象的属性,Z可以不通过其他对象,直接获取到该属性的值

因此,单例对象的构造方法应该是私有的

1
2
3
4
5
6
7
8
9
10
11
class Singleton{
private $props = array();
private function __construct(){
}
public function setProperty($key,$value){
$this->props[$key] = $value;
}
public function getProperty($key){
return $this->pros[$key];
}
}

但是这样是不能用的,因为构造方法是私有的。所以需要静态方法和静态属性来间接完成实例化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Singleton{
private $props = array();
private static $instance;
private function __construct(){
}
public function setProperty($key,$value){
$this->props[$key] = $value;
}
public function getProperty($key){
return $this->props[$key];
}
public static function getInstance(){
if(empty(self::$instance)){
self::$instance = new Singleton();
}
return self::$instance;
}
}

单例模式和全局变量依然会被误用,因为它可以在任何地方被使用,所以很难调试它的依赖关系(比如公司的框架,对pdo进行了封装,而在对某些语句进行跟踪调试时,就变的困难了,因为不只是一处去调用该模块)
关于单例模式,java中有很多种实现方式,每一种都有自己的优缺点,另外java中有时候单例模式有时候会失效,这些都是语言层级了,就不多写了

关于Varnish

  • 高性能的开源HTTP加速器
  • 基于内存进行缓存,重启后数据将消失
  • 利用虚拟内存方式,I/O性能好
  • 配置语言VCL(Varnish Configuration Language) 灵活管理配置.
    在程序启动时,varnish就把VCL转换成二进制代码,因此性能非常高

安装

略。注意安装依赖,有些命令需要安装依赖


varnish 启动

  • 一个启动的例子

    1
    2
    3
    4
    5
    6
    7
    8
    sudo /usr/local/varnish/sbin/varnishd \
    -u www -g www \
    -f /usr/local/varnish/etc/varnish/default.vcl \
    -a 127.0.0.1:8080 \
    -s file,/var/www/varnish_cache.data,1G \
    -w 1024,2048,10 \
    -t 3600 \
    -T 127.0.0.1:2000
  • 说明

    1
    2
    3
    4
    5
    6
    7
    8
    u 用户名
    g 所属组
    f 配置文件地址
    a 绑定地址及端口
    s 缓存文件地址及大小
    w 最大最小线程 及超时时间
    T 管理端口 主要用来清除缓存
    t 默认缓存时间
  • 结束

    `sudo pkill varnishd`
    

管理命令 varnishadm

1
2
3
4
5
6
7
/usr/local/varnish/bin/varnishadm -T 127.0.0.1:2000 -S /etc/varnish/secret help 查看可用的命令列表 -S 参数为指定密码文件
vcl.load default.vcl /etc/varnish/default.vcl 载入新vcl配置文件
vcl.list 查看已有的配置列表
vcl.use default.vcl 使用加载的配置文件
ban.url ^hello.php$ 清除hello.php的缓存
start 启动
stop 关闭

其他命令

1
2
varnishlog 日志
varnishstat 状态 :命中等

VCL

1.三个重要的数据结构

  • req
    请求头信息 当varnish接收到请求的时候 req会被创建
  • beresp
    后端返回对象 包含从backend返回的头信息
  • obj
    缓存了的对象。大多数是驻留在内存中的只读对象。obj.ttl是可以写的,剩下的都是只读的

更多变量点这里

2.子程序
VCL 文件被分为多个子程序 常用子程序

  • vcl_recv 当有访问时,varnish会执行该子程序,该程序通过对req的一些参数进行处理,决定接下来的动作 这里只能访问到req数据结构
    一般以以下3种动作结束
    • pass:执行pass动作,把请求交给vcl_pass模块处理。
    • pipe:执行pipe动作,把请求交给vcl_pipe模块处理。
    • error code [reason]:表示把错误标识返回给客户端,并放弃处理该请求。错误标识包括200、405等。”reason”是对错误的提示信息。
  • vcl_fetch 当varnish成功从后端返回数据时,执行该子程序 这里既能访问req 也能访问beresp
  • vcl_pipe 执行pipe动作时调用,用于将请求直接传递至后端主机,在请求和返回的内容没有改变的情况下,
    也就是在当前连接未关闭时,服务器将不变的内容返回给客户端,直到该连接被关闭
  • lookup 一个请求在vcl_recv中被lookup后,Varnish将在缓存中提取数据。
    如果缓存中有相应的数据,就把控制权交给vcl_hit子程序;
    如果缓存中没有相应的数据,请求将被设置为pass并将其交给vcl_miss子程序

varnish 将在不同阶段执行它的子程序代码,因为它的代码是一行一行执行的,不存在优先级问题。随时可以调用这个子程序中的功能并且当他执行完成后就退出。

3.动作(action):

  • pass: 当一个请求被 pass 后,这个请求将通过 varnish 转发到后端服务器,但是它不会被缓存。pass 可以放在 vcl_recv 和 vcl_fetch 中。
  • lookup: 当一个请求在 vcl_recv 中被 lookup 后,varnish 将从缓存中提取数据,如果缓存中没有数据,将被设置为 pass,不能在 vcl_fetch 中设置 lookup。
  • pipe: pipe 和 pass 相似,都要访问后端服务器,不过当进入 pipe 模式后,在此连接未关闭前,后续的所有请求都发到后端服务器,而pass只对当前的传输有效,后续的请求会根据策略决定是否发送到客户端
  • deliver: 请求的目标被缓存,然后发送给客户端。
  • esi: ESI-process the fetched document (Edge Side Includes ,使用varnish+esi
    可以对用户个性化相关的进行缓存,而我们通常的做法是使用ajax+memcache对用户个性化的内容进行缓存 官方文档

处理流程

image


http头信息

HTTP协议定义了四个可以用来控制浏览器缓存的HTTP头,它们是:

  • Last-Modified
  • Expires
  • Pragma: no-cache
  • Cache-Control

在HTTP/1.0协议中:

  • Last-Modified是控制缓存的一个非常重要的HTTP头。
    如果需要控制浏览器的缓存,服务器首先必须发送一个 以UTC时间为值的Last-Modifeid头,
    当第二次访问这个页面时,浏览器会发送一个If-Modified-Since头给服务器,让服务器判断是否有必要更新内容,
    这个If-Modified-Since头的值就是上次访问页面时,浏览器发送的Last-Modifeid头的值。
  • Expires是HTTP/1.0中另外一个很重要的HTTP头,它表示缓存的存在时间,
    告诉客户端浏览器在这个时间之前不对服务器发送请求,而直接使用浏览器的缓存。
    在HTTP/1.0中,可以使用Pragma: no-cache头来告诉浏览器不要缓存内容,
    它相当于HTTP/1.1中的Cache-Control:no-cache

HTTP/1.0协议的这种实现方式的缺点是,服务器和客户端的时间有可能是不同步的,
这样会造成缓存的实现达不到预期效果。HTTP/1.1协议用Cache-Control头解决了这个问题

在http 1.1中
Cache-Control响应头的语法为:Cache-Control = “Cache-Control” “:”{缓存响应指令}
缓存响应指令为一下几个中的任意一个:

  • public 指示响应数据可以被任何客户端缓存

  • private 指示响应数据可以被非共享缓存所缓存。这表明响应的数据可以被发送请求的浏览器缓存,而不能被中介所缓存

  • no-cache 指示响应数据不能被任何接受响应的客户端所缓存

  • no-store 指示所传送的响应数据除了不能被缓存,也不能存入磁盘。一般用于敏感数据,以免数据被复制。

  • must-revalidate 指示所有的缓存都必须重新验证,
    在这个过程中,浏览器会发送一个If-Modified-Since头。
    如果服务器程序验证得出当前的响应数据为最新的数 据,那么服务器应当返回一个304

  • proxy-revalidate 与must-revalidate相似,不同的是用来指示共享缓存

  • max-age=时间 数据经过max-age设置的秒数后就会失效,
    相当于HTTP/1.0中的Expires头。
    如果在一次响应中同时设置了max-age和Expires,那么max-age将具有较高的优先级

  • s-maxage=时间 与max-age相似,不同的是用来指示共享缓存。
    注:通过POST方法发送的请求不能以如上所述的方式缓存

可以在使用浏览器发送如下信息头

  • 当我们在浏览器中输入url网址时,如果有过该网址的访问记录,浏览器会判断 max-age,如果没有会发送If-Modified-Since头信息,服务器去处理是否有更新
    有些内容有max-age,浏览器不会发送请求,使用浏览器缓存,Chrome返回的状态码是200 OK (from cache),firefox没有请求。

  • 当我们按下F5时,浏览器会发送If-Modified-Since,max-age=0 头信息 这样服务器就会返回其最新内容(如果有varnish缓存的话,只返回varnish缓存)

  • 当我们按下Control + F5,强刷,浏览器会发送 Pragma: no-cache,Cache-Control: no-cache信息头 如果在vcl中对这类信息头做处理,就可以刷新varnish缓存
    这在生产环境中很适用,但是在线上环境中所有人都可以通过强制刷新varnish.所以需要使用白名单允许一些ip可以清除缓存,通常Cache-Control:nocache 是被varnish忽略的,根据自身需要处理

当然也可以写代码发送请求,或是使用浏览器插件比如livehttpheader或者使用curl命令


清除缓存

1.这两个规则定义了允许哪些主机通过HTTP来执行PURGE进行缓存删除。如果不是指定的IP,就会出现HTTP 405错误,提示Not allowed错误字样

1
2
3
4
5
6
7
8
9
10
acl purge {
"localhost";
"127.0.0.1";
}
if (req.request == "PURGE") {
if (!client.ip ~ purge) {
error 405 "Not allowed.";
}
return(lookup);
}

2.varnishadm ban.url [regexp] 不需要加域名

注意事项

  • 如果backend返回的头信息中包含setcookie,varnish不会对此缓存,一般会在vcl中unset掉backend header产生的cookie 更多请看这里

    php中有两种set cookie方法:
    一种是header(string,replace,http_response_code),
    一种是setCookie(name,value,expire,path,domain,secure)
    setCookie设置的cookie,Varnish会缓存吗?(其实是一样的)

  • 如果varnish看到授权头信息(Authorization)时,它会pass该请求。可以unset这个头信息

  • 当几个客户端正访问相同页面时,varnish会发送一个请求到后端,然后让那个其他几个请求挂起等待返回结果,
    返回结果后,复制请求的结果发送给客户端并且让其他请求等待,
    但是如果,每秒上千的请求同时呢?(这个队列很庞大),这时候我们期待的应该是,返回过期的cache给用户
    为了使用过期的cache给用户提供服务,我们需要增加他们的TTL,保存所有cache中的内容在TTL过期以后30分钟不删除,
    使用以下VCL:

    1
    2
    3
    sub vcl_fetch {
    set beresp.grace = 30m;
    }

    Varnish还不会使用过期的目标给用户提供服务,所以我们需要配置以下代码,在cache过期后的15秒内,使用旧的内容提供服务:

    1
    2
    3
    sub vcl_recv {
    set req.grace = 15s;
    }
  • 如果后端出了问题,是不是不应该返回问题的页面,而应该返回就版本的正常页面,可以开启健康检查,如果后端出问题了,可以长时间提供旧版本的内容

    1
    2
    3
    4
    5
    if (! req.backend.healthy) {
    set req.grace = 5m;
    } else {
    set req.grace = 15s;
    }

总结

说一下我遇到的一些坑吧,毕竟不是运维人员,很多监控还不是很懂,所以只是说一些代码层面上的。

  • 一般来说varnish是要将后端返回的header头的cookie信息重置掉,这样可以高效地利用缓存。
    这种方式带来的后果就是:
    由于Varnish 是一个HTTP缓存服务器,也就是说它是直接和浏览器打交道的,更通俗点,Varnish缓存的内容是直接被浏览器解析的!
    所以 不要在后端服务器中返回带有状态的代码!Varnish对于所有用户访问的同一链接,返回的都是同一个内容。
    如果你在后端返回的信息存在cookie,并且在Varnish中将cookie干掉了,就会导致,一个用户的行为被Varnish缓存住了,其他用户也被动地进行了这个行为。
    通常解决这种方式的做法是使用ajax。

  • varnish 配置文件是允许重复配置同一个动作的,但是只是先加载的有效= =#。所以注意这个坑(已测试)

  • 由于我们的业务需要,测试环境是穿透varnish的,也就是白名单,所以很多时候,内网会泄漏掉varnish的很多东西
    这时候需要使用一些外网IP,挂代理取检测网站内容了(不过要小心,外网代理的策略有可能是把整个网页缓存了)

  • 由于Varnish会对每一个url地址做缓存,为了不必要的浪费,一般是对某个url做截断处理
    (比如:http:www.baidu.com/?a=1&b=1 和http:www.baidu.com/ 缓存的应该是同一个副本)

参考文献

以前分享的文章, 放到博客里吧