letsencrypt实践
Saturday, December 12, 2015

随着互联网的发展,网络安全成了一个越来越重要的话题,而作为一个web开发者,使用https部署自己的站点成为了安全的重要一步,废话完毕。

不过话说回来,许多http2的实现都是只基于tls的。个人站点如果要配置https,就得去买https的证书,买是要化钱的,虽然也花不了几个钱。不过现在有免费的选择,那就是Let’s Encrypt

先简单介绍一下Let’s Encrypt,他也是一家证书颁发机构(CA),旨在通过提供免费、自动、开放的证书服务,以达到天下无贼的目的。工作过程也很简单,你有一个域名,想进行加密,请求Let’s Encrypt给你发一个证书,Let’s Encrypt首先需要确保这个域名是你的,确认之后会发放对应域名的整数,你就可以用这些整数配置你的web服务使用https了。

Let’s Encrypt验证域名的方式有两种,一是提供域名的DNS记录,二是提供一个域名下的资源。说一下第二种方式,当Let’s Encrypt的agent向Let’s Encrypt请求域名(example.com)的证书时,Let’s Encrypt会生成一个私钥和一个随机数,agent会对随机数进行加密,把加密之后的内容放到指定的路径(/.well-known/acme-challenge/)下,如果这个域名是你的,那么加密后的内容可以通过http://example.com/.well-known/acme-challenge/xxxx被访问到,Let’s Encrypt通过验证加密后的内容是否匹配就可以确定域名的拥有者了。

请求证书的方式也是有agent发起,会使用之前颁发的私钥做加密。真详细的描述可以看这里

安装方式也很简单:

git clone https://github.com/letsencrypt/letsencrypt
cd letsencrypt
./letsencrypt-auto

Let’s Encrypt提供了多种方式来获取和使用整数:apache、nginx、webroot、standlone。apache和nginx都是利用相应的模块进行整数的获取。standlone是指启动一个独立的web服务来获取整数,这需要你的机器上的80端口或443端口是可用的。webroot是只利用现有的web服务来获取整数。我使用了webroot这种方式,执行以下命令: letsencrypt certonly --webroot -w /home/docker/usr/share/nginx/html -d blog.bug1874.com

生成的证书在/etc/letsencrypt/live/bug1874.com下,会生成一下几个文件cert.pem、chain.pem、fullchain.pem、privkey.pem。然后是配置nginx:

listen 443 ssl;
ssl_certificate /etc/letsencrypt/live/blog.bug1874.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/blog.bug1874.com/privkey.pem;

Apache的https配置我就不帖了,有需要的自己google去。

上面写的内容Let’s Encrypt的文档上都有,比我说的清楚明白,感兴趣的同学可以去瞄瞄。

 
MySQL双主master-master配置实践
Saturday, October 24, 2015

一直想着要自己亲手配置MySQL双主(master-master)相互同步的配置,趁着今天没有导出乱跑,吃完午饭就开始鼓捣,一下午终于鼓捣完了,现在把过程记录一下。

MySQL的环境直接通过docker做部署的,image使用的是mysql:5.7,因为需要修改MySQL的配置,所以就先启动了一个名为test-mysql的容器,然后通过docker cp命令把MySQL的配置文件和数据文件复制到宿主机上:

docker cp test-mysql:/etc/mysql/ /data/mysql_1/conf/
docker cp test-mysql:/var/lib/mysql /data/mysql_1/data/
docker cp test-mysql:/etc/mysql/ /data/mysql_2/conf/
docker cp test-mysql:/var/lib/mysql /data/mysql_2/data/

修改MySQL的配置文件/data/mysql_1/conf/my.cnf,增加以下两列配置:

server-id       = 1
log-bin         = mysql-bin

/data/mysql_1/conf/my.cnf文件增加下列配置:

server-id       = 1
log-bin         = mysql-bin

同时需要把/data/mysql_1/data/auto.conf文件删掉,这个文件是由MySQL生成的,文件里存有server-uuid,第一次配置的时候没有做这一步,slave在启动时报错:Last_IO_Error: Fatal error: The slave I/O thread stops because master and slave have equal MySQL server UUIDs; these UUIDs must be different for replication to work.

然后是启动MySQL,mysql_1和mysql_2两个容器:

docker run --name mysql_1 -p 3331:3306 -v /data/mysql_1/conf:/etc/mysql -v /data/mysql_1/data:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=root -d mysql:5.7
docker run --name mysql_2 -p 3332:3306 -v /data/mysql_2/conf:/etc/mysql -v /data/mysql_2/data:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=root -d mysql:5.7

连接到mysql_1,配置相应的MySQL replication配置:

mysql -h0.0.0.0 -P3331 -uroot -proot
create user 'replicator'@'%' identified by 'replicator';
grant replication slave on *.* to 'replicator'@'%'; 
CHANGE MASTER TO MASTER_HOST='172.17.0.13',MASTER_USER='replicator',MASTER_PASSWORD='replicator',MASTER_LOG_FILE='mysql-bin.000001',MASTER_LOG_POS=1305;
start slave;

其中,MASTER_HOST指向的ip为mysql_2的容器ip,可以通过docker inspect --format ''命令查看。MASTER_LOG_FILE、MASTER_LOG_POS可以在mysql_2下通过show master status查看。mysql_2的主从配置也做相应的配置即可。 show-master-status.jpg

到此所有的配置都完成,在mysql_1下创建一个数据库,在mysql_2下就可以看到了。

配置过程中参考了此篇文章,在此表示感谢。

 
将Blog改成了响应式
Saturday, November 01, 2014

其实用这个blog就写了两篇文章,可能是我道行尚浅,总觉得没有太多东西可写,有些东西,写出来肤浅,而有些东西写出来,又矫情,再加上懒惰,所以写的更少了。只能说三个字,药不能停。

前些时候,突然觉得应该把我的这个不怎么写的blog改成响应式的,于是上个周五的晚上,花了一晚上的时间改了一下,这个礼拜,花点时间记录一下。

所谓的响应式设计(教程),其实就是根据访问站点的终端的尺寸,动态的调整页面的布局。而实现这个功能的关键就是media queries。media queries支持查询当前设备的屏幕尺寸(device-width),屏幕方向(orientation)等等。然后针对不同的尺寸设置不同的样式。举个例子:

@media (max-width: 480px) {
	.small_screen {
		max-width: 100%;
	}
}

上面这段css,.small_screen样式只会在宽度小于480px的时候有效。

注意这里有一个logical pixel和physical pixel的区别(参考),所谓的logical pixel就是css获取到的像素值,而physical pixel是屏幕的物理尺寸。两者并不一定相等,取决于你屏幕的分辨率。所以需要设置viewport这个meta flag让logical pixel适配physical pixel。通常的设置为

<meta name=”viewport” content=”width=device-width initial-scale=1”>

而我在做的过程过发现即使设置了viewport,不同的浏览器表现也是不一样的。当我设置body的属性为width:100%时,在chrome下表现是正常的,但是在Android的原生浏览器下,body的宽度变成了屏幕宽度的两倍(我用得是note2,屏幕宽度是360,在Android原生浏览器下body宽度变成了720)。window.screen.width和window.innerWidth两个属性的值,前者是720,后者是360。google出来的结果大部分都是建议设置viewport,怎么都搞不定。

不得已只能用比较山寨的方法,用js判断scree.width和innerWidth两个值是否相等,不等的话就设置body的宽度为较小的值。设置了style之后发现在Android原生浏览器下还是没有效果。设置的宽度没有生效,理论上内联样式的优先级是最高的,不知道怎么在Android原生浏览器下优先级反而没有class的优先级高,设置了!important才达到预期的效果。

 
MySQL死锁案例分享
Saturday, August 09, 2014

以前觉得INNODB的row lock机制很难碰到死锁,开启事务就更不用担心,之前的确没有碰到过死锁的问题。不过最近在项目中却碰到了死锁的问题。

问题出现的场景简化如下:

DB的表结构:

CREATE TABLE `test` (
  		`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  		`name` char(50) NOT NULL DEFAULT '',
  		PRIMARY KEY (`id`),
	UNIQUE KEY `name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8

业务逻辑简化为:

mysqli_begin_transation()
for () {
	mysqli_query("REPLACE INTO test (name) values ('xxx')");
}
mysqli_commit(); 其实就是会循环往test表里插入数据。针对这样的场景做了如下的测试 script1.php

<?php
$db = new mysqli('localhost', 'root', '', 'test');
$db->begin_transaction();
$db->query("REPLACE INTO test (name) value ('a')");

sleep(10);

$db->query("REPLACE INTO test (name) value ('a')");
if (!empty($db->error)) {
    echo $db->error . PHP_EOL;
}

$db->commit();

script2.php

<?php
$db = new mysqli('localhost', 'root', '', 'test');
$db->begin_transaction();
$db->query("REPLACE INTO test (name) value ('b')");

if (!empty($db->error)) {
    echo $db->error . PHP_EOL;
}

$db->commit();

script2.php紧接着script1.php运行,script2.php会报错:Deadlock found when trying to get lock; try restarting transaction。show engine innodb status看到的确发生了死锁,死锁的原因是因为script1的第一条语句的锁和script2中的replace语句发生了锁冲突,script1的第二条语句又再次申请锁时发生了死锁。 一开始很难理解为什么script1中的第二条语句还会再次申请锁,而且锁的类型和前一条语句锁的类型不一直,于是google了一下,发现MySQL对replace的实现其实是delete+insert。让我们先忘掉replace看另外一个例子: script3.php

<?php
$db = new mysqli('localhost', 'root', '', 'test');
$db->begin_transaction();

$db->query("UPDATE test SET name='a' WHERE name = 'a'");
sleep(5);
$db->query("INSERT IGNORE INTO test (name) value ('a')");

if (!empty($db->error)) {
    echo $db->error;
}

$db->commit();

script4.php

<?php
$db = new mysqli('localhost', 'root', '', 'test');
$db->begin_transaction();

$db->query("UPDATE test SET name='a' WHERE name = 'a'");

if (!empty($db->error)) {
    echo $db->error;
}

$db->commit();

scirpt3.php和script4.php先后执行也会出现死锁,产生死锁的原因为:

  1. script3中的update语句会获得一个排他的锁
  2. script4中的update语句想要获得一个排它锁,这个加锁请求会被排队
  3. script3中的insert ignore语句想要获得一个共享锁,这个锁请求也会被排队,死锁出现

script3中的insert会加共享锁是因为当出现重复键的时候,insert会加一个共享锁到相应的记录。如果把name字段上的unique key去掉变不会出现死锁。

个人理解产生死锁的根本原因是因为update和insert加锁的类型是不一样的,update和delete会加 exclusive next-key lock,而insert加的是index-record lock。第二个问题很久很久之前有人曾提过bug,不过对bug的解释不是很明白。

回到第一个问题,如果把replace into改成insert on dumplicate key update,死锁也会消失。很多人给出的建议也似尽量避免使用replace into。

关于这个问题其实我解释的也不是很清楚,有人如果能更清楚的解释这个问题欢迎留言交流。

关于MySQL锁机制可以看这里这里

 
关于PHP字符串比较的一次探究
Friday, October 11, 2013

昨天无意间看到有人抱怨PHP的==的判断,下面的这行代码的输出结果会是TRUE :

php -r "var_dump('10' == '1E1');"

觉得挺奇怪的,就想看看PHP到底是怎么执行==操作的,通过vld输出opcode

php -dvld.active=1 -r "var_dump('10' == '1E2');"

可以看到==对应的opcode是IS_EQUAL,于是找到IS_EQUAL对应的VM handler,所有的opcode的VM handler定义都在PHP源文件下的Zend/zend_vm_def.h中

ZVAL_BOOL(result, fast_equal_function(result,
         GET_OP1_ZVAL_PTR(BP_VAR_R),
         GET_OP2_ZVAL_PTR(BP_VAR_R) TSRMLS_CC));

可以看到比较是通过调用fast_equal_function这个函数来完成的,fast_equal_function又调用了compare_function,一直跟下去,发现最终调用了zend_strtod这个函数将字符串转成数字再做比较,zend_strtod的定义如下

ZEND_API double zend_strtod (CONST char *s00, CONST char **se)

不过很囧的是,这个函数太长了,看了半天都没看懂,不过"1E1"对应的最终调用是zend_strtod("1E1", "E1"),于是写了一个扩展看这个函数调用到底输出多少

PHP_FUNCTION(sample_strtod)
{
	char s[4];
	char se[3];
	sprintf(s, "1E1");
	sprintf(se, "E1");
	double ret;
	ret = zend_strtod(s, se);
	RETURN_DOUBLE(ret);
}

函数的最终返回是10。

开始看了半天都搞不懂1E1为什么会被转成数字10,后面突然发现mia的这不就是所谓的科学计数法嘛,1E1 = 1*10^1…